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.
This commit is contained in:
1 parent
a82d8cfb05
commit
0dcc05ebf9
15 files changed
+1885
-1873
No files matched your search
@@ -0,0 +1,312 @@
|
||||
// ── User Content Rendering ──
|
||||
|
||||
function renderUserContent(container, text, inlineImages) {
|
||||
const raw = (text || '').trim();
|
||||
|
||||
// Reply-Zitat erkennen und als visuellen Block rendern
|
||||
// Format: > [Zitat: HH:MM, Role]:\n> text...\n\nrest
|
||||
const quoteMatch = raw.match(/^> \[Zitat: ([^\]]+)\]:\n((?:> .*\n?)*)\n*([\s\S]*)$/);
|
||||
if (quoteMatch) {
|
||||
const quoteMeta = quoteMatch[1]; // z.B. "14:32, Assistant"
|
||||
const quoteBody = quoteMatch[2].replace(/^> /gm, '').trim();
|
||||
const restText = quoteMatch[3].trim();
|
||||
|
||||
const quoteEl = document.createElement('div');
|
||||
quoteEl.className = 'reply-quote';
|
||||
const metaEl = document.createElement('div');
|
||||
metaEl.className = 'reply-quote-meta';
|
||||
metaEl.innerHTML = '<i class="fa fa-reply"></i> ' + esc(quoteMeta);
|
||||
quoteEl.appendChild(metaEl);
|
||||
const bodyEl = document.createElement('div');
|
||||
bodyEl.className = 'reply-quote-text';
|
||||
bodyEl.textContent = truncate(quoteBody, 200);
|
||||
quoteEl.appendChild(bodyEl);
|
||||
container.appendChild(quoteEl);
|
||||
|
||||
// Rest als normalen Text weiterverarbeiten
|
||||
if (restText) {
|
||||
_renderUserTextContent(container, restText, inlineImages);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
_renderUserTextContent(container, raw, inlineImages);
|
||||
}
|
||||
|
||||
function _renderUserTextContent(container, raw, inlineImages) {
|
||||
// [Bild: name] durch Platzhalter ersetzen
|
||||
const imagePattern = /\[Bild: ([^\]]+)\]/g;
|
||||
const imageRefs = [];
|
||||
let mdText = raw.replace(imagePattern, function(m, name) {
|
||||
imageRefs.push(name);
|
||||
return '%%IMGREF' + (imageRefs.length - 1) + '%%';
|
||||
});
|
||||
|
||||
// [Datei: path] oder [Datei: path:1-10] durch Platzhalter ersetzen (vor Markdown-Parsing)
|
||||
const pattern = /\[Datei: ([^\]]+)\]/g;
|
||||
const fileRefs = [];
|
||||
mdText = mdText.replace(pattern, function(m, ref) {
|
||||
fileRefs.push(ref);
|
||||
return '%%FILEREF' + (fileRefs.length - 1) + '%%';
|
||||
});
|
||||
|
||||
// Markdown rendern
|
||||
let html = renderMarkdown(mdText);
|
||||
|
||||
// Bild-Platzhalter durch Inline-Bilder ersetzen
|
||||
html = html.replace(/%%IMGREF(\d+)%%/g, function(m, idx) {
|
||||
const name = imageRefs[parseInt(idx)];
|
||||
// Wenn Inline-Bild-Daten vorhanden (beim Senden), direkt anzeigen
|
||||
const img = inlineImages && inlineImages.find(function(i) { return i.name === name; });
|
||||
if (img) {
|
||||
return '<img class="inline-user-image" src="' + esc(img.dataUrl) + '" alt="' + esc(name) + '" title="' + esc(name) + '">';
|
||||
}
|
||||
// Sonst: Platzhalter mit data-Attribut für späteres Laden
|
||||
return '<span class="inline-image-placeholder" data-filename="' + esc(name) + '">'
|
||||
+ '<i class="fa fa-image"></i> ' + esc(name) + '</span>';
|
||||
});
|
||||
|
||||
// Datei-Platzhalter durch File-Ref-Chips ersetzen (als HTML)
|
||||
html = html.replace(/%%FILEREF(\d+)%%/g, function(m, idx) {
|
||||
const ref = fileRefs[parseInt(idx)];
|
||||
const filePath = ref.replace(/:\d+(-\d+)?$/, '');
|
||||
const fileName = filePath.includes('/') ? filePath.substring(filePath.lastIndexOf('/') + 1) : filePath;
|
||||
const lineInfo = ref.replace(filePath, '').trim();
|
||||
return '<span class="file-ref-chip" data-filepath="' + esc(filePath) + '" title="' + esc(ref) + '" style="cursor:pointer">'
|
||||
+ '<i class="fa fa-file-text-o"></i> ' + esc(fileName + lineInfo) + '</span>';
|
||||
});
|
||||
|
||||
// In ein Wrapper-Div rendern (damit vorherige Elemente wie Reply-Quotes erhalten bleiben)
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'user-text-content';
|
||||
wrap.innerHTML = html;
|
||||
container.appendChild(wrap);
|
||||
|
||||
// Click-Handler für File-Ref-Chips nachträglich binden
|
||||
wrap.querySelectorAll('.file-ref-chip[data-filepath]').forEach(function(chip) {
|
||||
chip.addEventListener('click', function() {
|
||||
openFileExplorer({ sessionId: getCurrentSessionId(), initialFile: chip.dataset.filepath });
|
||||
});
|
||||
});
|
||||
|
||||
// Click-Handler für Inline-Bilder (Vollbild)
|
||||
wrap.querySelectorAll('.inline-user-image').forEach(function(img) {
|
||||
img.addEventListener('click', function() {
|
||||
showKreaImageModal(img.alt || 'Bild', img.src);
|
||||
});
|
||||
});
|
||||
|
||||
// Fluent Emoji DOM-Parsing
|
||||
parseFluentEmojis(wrap);
|
||||
}
|
||||
|
||||
// ── Message Bubbles ──
|
||||
|
||||
function addUserBubble(text, streamed = false, timestamp = '', messageId = null, isMarked = false, deletedOn = null, inlineImages = null) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg user markdown' + (isMarked ? ' marked' : '');
|
||||
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'user';
|
||||
div.dataset.timestamp = timestamp || new Date().toISOString();
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
if (streamed) div.classList.add('streamed');
|
||||
renderUserContent(div, text, inlineImages);
|
||||
_addTimestamp(div, timestamp);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, messageId, deletedOn);
|
||||
_addMarkButton(div, messageId, isMarked);
|
||||
_addShareButton(div, messageId);
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
function addAssistantBubble(text, timestamp = '', messageId = null, isMarked = false, deletedOn = null) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg assistant markdown' + (isMarked ? ' marked' : '');
|
||||
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'assistant';
|
||||
div.dataset.timestamp = timestamp || new Date().toISOString();
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
div.innerHTML = renderMarkdown(text);
|
||||
parseFluentEmojis(div);
|
||||
wrapKreaGalleries(div);
|
||||
_addTimestamp(div, timestamp);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, messageId, deletedOn);
|
||||
_addMarkButton(div, messageId, isMarked);
|
||||
_addShareButton(div, messageId);
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
return div;
|
||||
}
|
||||
|
||||
function addErrorBubble(text, streamed = false) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg system-info';
|
||||
if (streamed) div.classList.add('streamed');
|
||||
div.style.color = 'var(--dangerous)';
|
||||
div.innerHTML = '<i class="fa fa-times-circle"></i> ' + esc((text || '').trim());
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
function addWarningBubble(text, streamed = false) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg system-info';
|
||||
if (streamed) div.classList.add('streamed');
|
||||
div.style.color = 'var(--warning)';
|
||||
div.innerHTML = '<i class="fa fa-exclamation-triangle"></i> ' + esc((text || '').trim());
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
}
|
||||
|
||||
// ── Choice Bubbles (ask_user_choice) ──
|
||||
|
||||
// Wird live aus dem SSE-Stream aufgerufen
|
||||
function addChoiceBubble(toolCallId, question, choices, streamed = true) {
|
||||
// Guard: choices muss ein Array sein
|
||||
if (!Array.isArray(choices)) choices = [String(choices)];
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg choice-bubble';
|
||||
if (streamed) div.classList.add('streamed');
|
||||
div.dataset.toolCallId = toolCallId;
|
||||
|
||||
const questionDiv = document.createElement('div');
|
||||
questionDiv.className = 'choice-question';
|
||||
questionDiv.innerHTML = renderMarkdown(question);
|
||||
parseFluentEmojis(questionDiv);
|
||||
div.appendChild(questionDiv);
|
||||
|
||||
const buttonsDiv = document.createElement('div');
|
||||
buttonsDiv.className = 'choice-buttons';
|
||||
for (const choice of choices) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'choice-btn';
|
||||
btn.textContent = choice;
|
||||
btn.addEventListener('click', () => handleChoiceClick(toolCallId, choice, div));
|
||||
buttonsDiv.appendChild(btn);
|
||||
}
|
||||
div.appendChild(buttonsDiv);
|
||||
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
return div;
|
||||
}
|
||||
|
||||
// Wird für History-Rendering aufgerufen (bereits beantwortete Choices)
|
||||
function addChoiceBubbleStatic(toolCallId, question, choices, selectedChoice, timestamp, messageId) {
|
||||
// Guard: choices muss ein Array sein
|
||||
if (!Array.isArray(choices)) choices = [String(choices)];
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg choice-bubble choice-answered';
|
||||
div.dataset.toolCallId = toolCallId;
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
|
||||
const questionDiv = document.createElement('div');
|
||||
questionDiv.className = 'choice-question';
|
||||
questionDiv.innerHTML = renderMarkdown(question);
|
||||
parseFluentEmojis(questionDiv);
|
||||
div.appendChild(questionDiv);
|
||||
|
||||
const buttonsDiv = document.createElement('div');
|
||||
buttonsDiv.className = 'choice-buttons';
|
||||
for (const choice of choices) {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'choice-btn';
|
||||
btn.disabled = true;
|
||||
btn.textContent = choice;
|
||||
if (choice === selectedChoice) {
|
||||
btn.classList.add('choice-selected');
|
||||
} else {
|
||||
btn.classList.add('choice-unselected');
|
||||
}
|
||||
buttonsDiv.appendChild(btn);
|
||||
}
|
||||
div.appendChild(buttonsDiv);
|
||||
|
||||
if (timestamp) _addTimestamp(div, timestamp);
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
return div;
|
||||
}
|
||||
|
||||
// ── Review Bubbles (Static / History) ──
|
||||
|
||||
function addReviewBubbleStatic(text, timestamp, messageId, isMarked, deletedOn) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'msg review-bubble markdown' + (isMarked ? ' marked' : '');
|
||||
if (deletedOn) div.classList.add(isDeletedVisible() ? 'msg-deleted' : 'msg-deleted-hidden');
|
||||
div.dataset.role = 'assistant';
|
||||
div.dataset.timestamp = timestamp || '';
|
||||
if (messageId) div.dataset.messageId = messageId;
|
||||
div.innerHTML = renderMarkdown(text);
|
||||
parseFluentEmojis(div);
|
||||
_addTimestamp(div, timestamp);
|
||||
_addReplyButton(div);
|
||||
_addDeleteButton(div, messageId, deletedOn);
|
||||
_addMarkButton(div, messageId, isMarked);
|
||||
_addShareButton(div, messageId);
|
||||
chatEl().appendChild(div);
|
||||
scrollDown();
|
||||
return div;
|
||||
}
|
||||
|
||||
// ── Attachment Indicator ──
|
||||
|
||||
function addAttachmentIndicator(messageId, count, namesJson) {
|
||||
const chat = chatEl();
|
||||
const lastMsg = chat.lastElementChild;
|
||||
if (!lastMsg) return;
|
||||
|
||||
let names = [];
|
||||
try { if (namesJson) names = JSON.parse(namesJson); } catch {}
|
||||
|
||||
const indicator = document.createElement('div');
|
||||
indicator.className = 'attachment-indicator';
|
||||
|
||||
const nameList = names.filter(n => n).join(', ');
|
||||
indicator.innerHTML = `<i class="fa fa-paperclip"></i> ${count} Datei${count > 1 ? 'en' : ''}` + (nameList ? `: ${esc(nameList)}` : '');
|
||||
indicator.style.cursor = 'pointer';
|
||||
indicator.title = 'Klicken zum Anzeigen';
|
||||
indicator.addEventListener('click', () => loadAndShowAttachments(messageId, indicator));
|
||||
lastMsg.appendChild(indicator);
|
||||
}
|
||||
|
||||
async function loadAndShowAttachments(messageId, indicator) {
|
||||
if (indicator.dataset.loaded) return;
|
||||
indicator.dataset.loaded = '1';
|
||||
indicator.innerHTML = '<i class="fa fa-paperclip"></i> Laden...';
|
||||
|
||||
try {
|
||||
const resp = await fetch(`/api/attachments/${messageId}`);
|
||||
const attachments = await resp.json();
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.className = 'attachment-container';
|
||||
|
||||
for (const att of attachments) {
|
||||
if (att.type === 'image') {
|
||||
const img = document.createElement('img');
|
||||
img.src = `data:${att.mediaType};base64,${att.data}`;
|
||||
img.className = 'attachment-image';
|
||||
img.title = att.fileName || 'Bild';
|
||||
container.appendChild(img);
|
||||
} else {
|
||||
const link = document.createElement('a');
|
||||
link.href = `data:${att.mediaType};base64,${att.data}`;
|
||||
link.download = att.fileName || 'datei';
|
||||
link.className = 'attachment-file';
|
||||
link.innerHTML = `<i class="fa fa-file-text-o"></i> ${esc(att.fileName || 'Dokument')}`;
|
||||
container.appendChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
indicator.innerHTML = `<i class="fa fa-paperclip"></i> ${attachments.length} Datei${attachments.length > 1 ? 'en' : ''}`;
|
||||
indicator.parentElement.appendChild(container);
|
||||
} catch (e) {
|
||||
indicator.innerHTML = '<i class="fa fa-paperclip"></i> Fehler beim Laden';
|
||||
indicator.dataset.loaded = '';
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user