Neues Tool 'ask_user_choice' analog zum AskUserQuestion aus dem Claude Agent SDK. Claude kann dem User Multiple-Choice-Fragen stellen, wenn es mehrere sinnvolle Wege gibt. Der User wählt per Button, Claude arbeitet mit der Antwort weiter. Backend: - ToolService: ask_user_choice Tool-Definition (question + choices[2-5]) - AnthropicService/ProcessStream: Tool wird NICHT ausgeführt, sondern als SSE-Event 'user_choice' an den Client gesendet. stopReason wird auf 'awaiting_user_choice' gesetzt (mit Guard gegen Überschreibung durch message_delta) - ChatEndpoint: Erkennung von Choice-Responses (choiceToolCallId + choiceResult), tool_result wird in die korrekte Chain gespeichert (wo der tool_use liegt), toolChoice wird auf 'auto' gesetzt damit Claude frei antworten kann - ChatService: FindChainWithToolCall() zum Auffinden der richtigen Chain, BuildHistoryFromDb() erweitert um Choice-Chain-Erkennung (wenn letzte Chain leer/nur tool_result → vorletzte Chain vollständig rekonstruieren) - ChatModels: ChatRequest um ChoiceToolCallId + ChoiceResult erweitert Frontend: - api.js: apiChatChoice() für Choice-Responses - chat.js: handleStreamEvent() als shared Handler extrahiert (sendMessage + handleChoiceClick nutzen denselben Code), handleChoiceClick() startet neuen Stream nach Button-Klick - ui.js: addChoiceBubble() (live/interaktiv) + addChoiceBubbleStatic() (History) - history.js: Choice-Bubbles in _renderMessagesIntoFragment + renderMessages, Voraus-Suche nach zugehörigem tool_result, unbeantwortet = klickbare Buttons - chat.css: Styling für Choice-Bubbles (Primary-Color-Outline-Buttons, Hover, Selected/Unselected States) Robustheit: - Backend-Fallback wenn Claude choices als String statt Array sendet - Frontend-Guard: Array.isArray(choices) an allen 4 Render-Stellen - Page-Refresh mit offener Choice → aktive Buttons werden korrekt gerendert
597 lines
23 KiB
JavaScript
597 lines
23 KiB
JavaScript
// ── History 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
|
||
}
|
||
}
|
||
|
||
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();
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
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:'));
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 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);
|
||
|
||
const archiveImageIds = [];
|
||
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';
|
||
if (msg.id) div.dataset.messageId = msg.id;
|
||
renderUserContent(div, msg.content);
|
||
_addTimestamp(div, msg.createdAt);
|
||
} else {
|
||
div.className = 'msg assistant markdown archived';
|
||
div.innerHTML = renderMarkdown(msg.content);
|
||
parseFluentEmojis(div);
|
||
_addTimestamp(div, msg.createdAt);
|
||
}
|
||
if (msg.role === 'user' && msg.attachmentCount > 0) {
|
||
if (/\[Bild: [^\]]+\]/.test(msg.content)) archiveImageIds.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);
|
||
}
|
||
break;
|
||
case 'tool_use':
|
||
if (!isToolVisible(msg.toolName)) continue;
|
||
div.className = 'tool-box archived';
|
||
renderToolBoxContent(div, msg.toolName, msg.content, null, false);
|
||
break;
|
||
case 'web_search_result': {
|
||
if (!isToolVisible('web_search')) continue;
|
||
var archSrc = [];
|
||
try { archSrc = JSON.parse(msg.content || '[]'); } catch(e) {}
|
||
if (archSrc.length === 0) continue; // Leere Suchergebnisse überspringen
|
||
div.className = 'tool-box archived';
|
||
div.dataset.toolName = 'web_search';
|
||
renderToolBoxContent(div, 'web_search', null, null, false, archSrc);
|
||
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);
|
||
|
||
// Bild-Attachments automatisch laden
|
||
for (const mid of archiveImageIds) _autoLoadImageAttachments(mid);
|
||
|
||
// 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;
|
||
}
|
||
}
|