Files
gott/wwwroot/js/history.js
T
Meleki 0dcc05ebf9 refactor: ui.js, chat.js, history.js in 13 modulare Dateien aufteilen
Die monolithische ui.js (1534 Zeilen) zusammen mit chat.js (612) und
history.js (626) in fokussierte Module aufgeteilt:

Neue Dateien aus ui.js:
- ui-helpers.js (126) - DOM-Utilities, $, esc, truncate, scrollDown, Toast
- markdown.js (143) - renderMarkdown, wrapKreaGalleries, LaTeX/Code-Schutz
- cards.js (90) - buildSessionCard, buildAgentCard
- msg-actions.js (208) - Delete, Mark, Share, Reply-Button, Timestamp
- tool-ui.js (332) - Tool-Boxes, Diffs, Todo-Liste, Visibility
- modals.js (190) - System-Prompt, Command-Output, Krea-Image Modals
- stream-ui.js (124) - Stream-Bubble, Review-Streaming
- bubbles.js (312) - User/Assistant/Error/Warning/Choice/Review Bubbles

Neue Dateien aus chat.js:
- reply.js (54) - Reply/Quote-Handling mit pendingReply State
- files.js (110) - File-Upload, Workspace-Refs mit pendingFiles/Refs State

Neue Datei aus history.js:
- archive.js (173) - Archiv-Loading mit loadedArchiveCount State

Verbleibend:
- chat.js (448) - Stream-Events, sendMessage, handleChoiceClick, Plan-Mode
- history.js (464) - History laden, Polling, Message-Rendering

Kein Code verändert, nur verschoben. Alle 94 Funktionen verifiziert.
Globale Variablen in die Module verschoben, wo sie primär genutzt werden.
2026-03-07 19:32:33 +01:00

465 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── Image Attachments Auto-Loading ──
async function _autoLoadImageAttachments(messageId) {
// Finde die User-Bubble mit dieser messageId
const bubble = document.querySelector('.msg.user[data-message-id="' + messageId + '"]');
if (!bubble) return;
const placeholders = bubble.querySelectorAll('.inline-image-placeholder');
if (placeholders.length === 0) return;
try {
const resp = await fetch('/api/attachments/' + messageId);
const attachments = await resp.json();
const imageAtts = attachments.filter(function(a) { return a.type === 'image'; });
placeholders.forEach(function(ph) {
const filename = ph.dataset.filename;
// Bild anhand Dateiname zuordnen, oder der Reihe nach
const att = imageAtts.find(function(a) { return a.fileName === filename; }) || imageAtts.shift();
if (att) {
const img = document.createElement('img');
img.className = 'inline-user-image';
img.src = 'data:' + att.mediaType + ';base64,' + att.data;
img.alt = filename;
img.title = filename;
img.addEventListener('click', function() { showKreaImageModal(filename, img.src); });
ph.replaceWith(img);
}
});
} catch (e) {
// Platzhalter bleiben stehen bei Fehler
}
}
// ── History State ──
let lastKnownMessageId = 0;
let _oldestLoadedMessageId = 0;
let _hasMoreOlderMessages = false;
let historyPollInterval = null;
// ── History Loading ──
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();
const imageMessageIds = [];
for (const entry of entries) {
_renderMessagesIntoFragment(fragment, entry.messages, imageMessageIds);
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);
}
// Bild-Attachments automatisch laden
for (const mid of imageMessageIds) _autoLoadImageAttachments(mid);
// 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);
}
}
}
// ── Helper Functions ──
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.length > 0 ? result[result.length - 1] : null;
const isReview = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Finalizer:'));
const prevIsReview = prev && prev.content && (prev.content.startsWith('[Review:') || prev.content.startsWith('[Finalizer:'));
if (prev && prev.type === 'text' && prev.role === 'assistant'
&& prev.iteration === msg.iteration && !isReview && !prevIsReview) {
prev.content = (prev.content || '') + ' ' + (msg.content || '');
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;
}
// ── Fragment Rendering (for loadOlderMessages) ──
function _renderMessagesIntoFragment(fragment, msgs, imageMessageIds) {
msgs = _mergeConsecutiveAssistantTexts(msgs);
for (var _msgIdx = 0; _msgIdx < msgs.length; _msgIdx++) {
const msg = msgs[_msgIdx];
const div = document.createElement('div');
switch (msg.type) {
case 'text': {
const isReviewMsg = msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Review-Feedback') || msg.content.startsWith('[Finalizer:') || msg.content.startsWith('[Preparer:'));
if (msg.role === 'user' && !isReviewMsg) {
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);
_addTimestamp(div, msg.createdAt);
_addReplyButton(div);
_addDeleteButton(div, msg.id, msg.deletedOn);
_addMarkButton(div, msg.id, msg.isMarked);
_addShareButton(div, msg.id);
fragment.appendChild(div);
if (msg.attachmentCount > 0) {
// Prüfe ob Bilder dabei sind (Content enthält [Bild: ...])
if (/\[Bild: [^\]]+\]/.test(msg.content) && imageMessageIds) {
imageMessageIds.push(msg.id);
}
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 (isReviewMsg) {
div.className = 'msg review-bubble markdown' + (msg.isMarked ? ' marked' : '');
if (msg.deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
div.dataset.role = msg.role;
div.dataset.timestamp = msg.createdAt || '';
if (msg.id) div.dataset.messageId = msg.id;
div.innerHTML = renderMarkdown(msg.content);
parseFluentEmojis(div);
_addTimestamp(div, msg.createdAt);
_addReplyButton(div);
_addDeleteButton(div, msg.id, msg.deletedOn);
_addMarkButton(div, msg.id, msg.isMarked);
_addShareButton(div, msg.id);
fragment.appendChild(div);
} 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);
_addTimestamp(div, msg.createdAt);
_addReplyButton(div);
_addDeleteButton(div, msg.id, msg.deletedOn);
_addMarkButton(div, msg.id, msg.isMarked);
_addShareButton(div, msg.id);
fragment.appendChild(div);
}
break;
}
case 'tool_use':
if (msg.toolName === 'ask_user_choice') {
// ask_user_choice: als Choice-Bubble rendern
try {
var choiceInput = typeof msg.content === 'string' ? JSON.parse(msg.content) : msg.content;
var choiceQuestion = choiceInput.question || '';
var choiceOptions = Array.isArray(choiceInput.choices) ? choiceInput.choices : [String(choiceInput.choices || '')];
// Voraus-Suche nach zugehörigem tool_result
var selectedChoice = null;
for (var ci = _msgIdx + 1; ci < msgs.length; ci++) {
if (msgs[ci].type === 'tool_result' && msgs[ci].toolCallId === msg.toolCallId) {
selectedChoice = msgs[ci].content;
msgs[ci]._choiceHandled = true; // Markieren damit es nicht nochmal gerendert wird
break;
}
}
var choiceDiv = document.createElement('div');
choiceDiv.className = 'msg choice-bubble' + (selectedChoice ? ' choice-answered' : '');
choiceDiv.dataset.toolCallId = msg.toolCallId || '';
if (msg.id) choiceDiv.dataset.messageId = msg.id;
var qDiv = document.createElement('div');
qDiv.className = 'choice-question';
qDiv.innerHTML = renderMarkdown(choiceQuestion);
parseFluentEmojis(qDiv);
choiceDiv.appendChild(qDiv);
var btnsDiv = document.createElement('div');
btnsDiv.className = 'choice-buttons';
for (var co of choiceOptions) {
var cbtn = document.createElement('button');
cbtn.className = 'choice-btn';
cbtn.textContent = co;
if (selectedChoice) {
cbtn.disabled = true;
if (co === selectedChoice) {
cbtn.classList.add('choice-selected');
} else {
cbtn.classList.add('choice-unselected');
}
} else {
// Noch nicht beantwortet – klickbar machen
(function(toolCallId, choice, bubble) {
cbtn.addEventListener('click', function() { handleChoiceClick(toolCallId, choice, bubble); });
})(msg.toolCallId, co, choiceDiv);
}
btnsDiv.appendChild(cbtn);
}
choiceDiv.appendChild(btnsDiv);
if (msg.createdAt) _addTimestamp(choiceDiv, msg.createdAt);
fragment.appendChild(choiceDiv);
} catch(e) {
console.error('Choice-Bubble Rendering fehlgeschlagen:', e);
}
continue;
}
if (!isToolVisible(msg.toolName)) continue;
div.className = 'tool-box';
renderToolBoxContent(div, msg.toolName, msg.content, null, false);
fragment.appendChild(div);
break;
case 'web_search_result': {
if (!isToolVisible('web_search')) continue;
var wsSources = [];
try { wsSources = JSON.parse(msg.content || '[]'); } catch(e) {}
if (wsSources.length === 0) continue; // Leere Suchergebnisse überspringen
div.className = 'tool-box';
div.dataset.toolName = 'web_search';
renderToolBoxContent(div, 'web_search', null, null, false, wsSources);
fragment.appendChild(div);
break;
}
case 'tool_result': {
// Überspringe tool_results die bereits von einer Choice-Bubble verarbeitet wurden
if (msg._choiceHandled) continue;
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;
}
}
}
// ── Render Messages (for pollHistory + loadHistory) ──
function renderMessages(msgs, addToConversation) {
msgs = _mergeConsecutiveAssistantTexts(msgs);
for (var _rmIdx = 0; _rmIdx < msgs.length; _rmIdx++) {
const msg = msgs[_rmIdx];
if (msg.id > lastKnownMessageId) lastKnownMessageId = msg.id;
switch (msg.type) {
case 'text':
if (msg.role === 'user') {
if (msg.content && msg.content.startsWith('[Review-Feedback')) {
addReviewBubbleStatic(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
} else {
addUserBubble(msg.content, false, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
if (msg.attachmentCount > 0) {
_autoLoadImageAttachments(msg.id);
addAttachmentIndicator(msg.id, msg.attachmentCount, msg.attachmentNames);
}
}
} else if (msg.role === 'assistant') {
if (msg.content && (msg.content.startsWith('[Review:') || msg.content.startsWith('[Finalizer:'))) {
addReviewBubbleStatic(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
} else {
addAssistantBubble(msg.content, msg.createdAt, msg.id, msg.isMarked, msg.deletedOn);
}
}
break;
case 'tool_use':
if (msg.toolName === 'ask_user_choice') {
try {
var choiceInput2 = typeof msg.content === 'string' ? JSON.parse(msg.content) : msg.content;
var choiceQ2 = choiceInput2.question || '';
var choiceOpts2 = Array.isArray(choiceInput2.choices) ? choiceInput2.choices : [String(choiceInput2.choices || '')];
// Voraus-Suche nach zugehörigem tool_result
var selectedChoice2 = null;
for (var ci2 = _rmIdx + 1; ci2 < msgs.length; ci2++) {
if (msgs[ci2].type === 'tool_result' && msgs[ci2].toolCallId === msg.toolCallId) {
selectedChoice2 = msgs[ci2].content;
msgs[ci2]._choiceHandled = true;
break;
}
}
if (selectedChoice2) {
addChoiceBubbleStatic(msg.toolCallId, choiceQ2, choiceOpts2, selectedChoice2, msg.createdAt, msg.id);
} else {
addChoiceBubble(msg.toolCallId, choiceQ2, choiceOpts2, false);
}
} catch(e) {
console.error('Choice-Bubble Rendering fehlgeschlagen:', e);
}
break;
}
if (isToolVisible(msg.toolName)) {
addToolBoxStatic(msg.toolName, msg.content, null, false);
}
break;
case 'web_search_result': {
if (isToolVisible('web_search')) {
var wsSrc = [];
try { wsSrc = JSON.parse(msg.content || '[]'); } catch(e) {}
if (wsSrc.length > 0) addToolBoxStatic('web_search', null, null, false, wsSrc);
}
break;
}
case 'tool_result':
if (msg._choiceHandled) break;
// 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;
}
}
}
// ── 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;
}
}