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.
209 lines
6.5 KiB
JavaScript
209 lines
6.5 KiB
JavaScript
// ── Timestamp ──
|
|
|
|
function _formatTimestamp(isoString) {
|
|
if (!isoString) return '';
|
|
var d = new Date(isoString);
|
|
if (isNaN(d.getTime())) return '';
|
|
var dd = String(d.getDate()).padStart(2, '0');
|
|
var mm = String(d.getMonth() + 1).padStart(2, '0');
|
|
var yyyy = d.getFullYear();
|
|
var hh = String(d.getHours()).padStart(2, '0');
|
|
var min = String(d.getMinutes()).padStart(2, '0');
|
|
var ss = String(d.getSeconds()).padStart(2, '0');
|
|
return dd + '.' + mm + '.' + yyyy + ' ' + hh + ':' + min + ':' + ss;
|
|
}
|
|
|
|
function _addTimestamp(div, timestamp) {
|
|
if (!timestamp) return;
|
|
var formatted = _formatTimestamp(timestamp);
|
|
if (!formatted) return;
|
|
var span = document.createElement('span');
|
|
span.className = 'msg-timestamp';
|
|
span.textContent = formatted;
|
|
div.prepend(span);
|
|
}
|
|
|
|
// ── Delete Button ──
|
|
|
|
function _addDeleteButton(div, messageId, deletedOn) {
|
|
if (!messageId) return;
|
|
const isDeleted = !!deletedOn;
|
|
const btn = document.createElement('button');
|
|
btn.className = 'msg-delete-btn icon-btn' + (isDeleted ? ' deleted' : '');
|
|
btn.innerHTML = '<i class="fa fa-trash-o"></i>';
|
|
btn.title = isDeleted ? 'Löschung aufheben' : 'Nachricht löschen';
|
|
btn.addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
try {
|
|
const result = await apiToggleMessageDelete(messageId);
|
|
const nowDeleted = !!result.deletedOn;
|
|
btn.classList.toggle('deleted', nowDeleted);
|
|
btn.title = nowDeleted ? 'Löschung aufheben' : 'Nachricht löschen';
|
|
if (nowDeleted && !isDeletedVisible()) {
|
|
// Nachricht ausblenden
|
|
div.classList.add('msg-deleted-hidden');
|
|
} else {
|
|
div.classList.remove('msg-deleted-hidden');
|
|
div.classList.toggle('msg-deleted', nowDeleted);
|
|
}
|
|
} catch (err) {
|
|
console.error('Delete toggle failed:', err);
|
|
}
|
|
});
|
|
// Nach dem Reply-Button einfügen
|
|
const replyBtn = div.querySelector('.msg-reply-btn');
|
|
if (replyBtn) {
|
|
replyBtn.after(btn);
|
|
} else {
|
|
div.prepend(btn);
|
|
}
|
|
}
|
|
|
|
function isDeletedVisible() {
|
|
return loadSettings().showDeletedMessages === true;
|
|
}
|
|
|
|
// ── Mark Button ──
|
|
|
|
function _addMarkButton(div, messageId, isMarked) {
|
|
if (!messageId) return; // kein DB-Eintrag → kein Button
|
|
const btn = document.createElement('button');
|
|
btn.className = 'msg-mark-btn icon-btn' + (isMarked ? ' marked' : '');
|
|
btn.innerHTML = isMarked ? '<i class="fa fa-star"></i>' : '<i class="fa fa-star-o"></i>';
|
|
btn.title = 'Als wichtig markieren';
|
|
btn.addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
try {
|
|
const result = await apiToggleMessageMark(messageId);
|
|
btn.classList.toggle('marked', result.isMarked);
|
|
btn.innerHTML = result.isMarked ? '<i class="fa fa-star"></i>' : '<i class="fa fa-star-o"></i>';
|
|
div.classList.toggle('marked', result.isMarked);
|
|
} catch (err) {
|
|
console.error('Mark toggle failed:', err);
|
|
}
|
|
});
|
|
// Nach dem Delete-Button einfügen (oder nach Reply-Button, oder am Anfang)
|
|
const deleteBtn = div.querySelector('.msg-delete-btn');
|
|
if (deleteBtn) {
|
|
deleteBtn.after(btn);
|
|
} else {
|
|
const replyBtn = div.querySelector('.msg-reply-btn');
|
|
if (replyBtn) {
|
|
replyBtn.after(btn);
|
|
} else {
|
|
div.prepend(btn);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Share Button ──
|
|
|
|
function _addShareButton(div, messageId) {
|
|
if (!messageId) return;
|
|
const btn = document.createElement('button');
|
|
btn.className = 'msg-share-btn icon-btn';
|
|
btn.innerHTML = '<i class="fa fa-share-alt"></i>';
|
|
btn.title = 'Nachricht teilen';
|
|
btn.addEventListener('click', async (e) => {
|
|
e.stopPropagation();
|
|
try {
|
|
// Kontextmenü: Einzelnachricht oder mit Kontext?
|
|
const mode = await _showShareMenu(btn);
|
|
if (!mode) return; // abgebrochen
|
|
|
|
const result = await apiShareMessage(messageId, mode === 'context');
|
|
const url = location.origin + '/#shared/' + result.token;
|
|
await navigator.clipboard.writeText(url);
|
|
showToast('Link kopiert!', 'success');
|
|
} catch (err) {
|
|
showToast('Fehler: ' + err.message, 'error');
|
|
}
|
|
});
|
|
// Nach dem Mark-Button einfügen
|
|
const markBtn = div.querySelector('.msg-mark-btn');
|
|
if (markBtn) {
|
|
markBtn.after(btn);
|
|
} else {
|
|
const deleteBtn = div.querySelector('.msg-delete-btn');
|
|
if (deleteBtn) {
|
|
deleteBtn.after(btn);
|
|
} else {
|
|
div.prepend(btn);
|
|
}
|
|
}
|
|
}
|
|
|
|
function _showShareMenu(anchorBtn) {
|
|
return new Promise((resolve) => {
|
|
// Wenn schon ein Menü offen, schließen
|
|
const existing = document.querySelector('.share-menu');
|
|
if (existing) { existing.remove(); resolve(null); return; }
|
|
|
|
const menu = document.createElement('div');
|
|
menu.className = 'share-menu';
|
|
menu.innerHTML = `
|
|
<button class="share-menu-item" data-mode="single">
|
|
<i class="fa fa-comment-o"></i> Einzelne Nachricht
|
|
</button>
|
|
<button class="share-menu-item" data-mode="context">
|
|
<i class="fa fa-comments-o"></i> Mit Kontext (Thread)
|
|
</button>
|
|
`;
|
|
|
|
// Positionierung
|
|
const rect = anchorBtn.getBoundingClientRect();
|
|
menu.style.position = 'fixed';
|
|
menu.style.top = (rect.bottom + 4) + 'px';
|
|
menu.style.left = rect.left + 'px';
|
|
menu.style.zIndex = '10000';
|
|
|
|
document.body.appendChild(menu);
|
|
|
|
function cleanup() {
|
|
menu.remove();
|
|
document.removeEventListener('click', outsideClick);
|
|
}
|
|
|
|
function outsideClick(e) {
|
|
if (!menu.contains(e.target)) {
|
|
cleanup();
|
|
resolve(null);
|
|
}
|
|
}
|
|
|
|
menu.querySelectorAll('.share-menu-item').forEach(item => {
|
|
item.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const mode = item.dataset.mode;
|
|
cleanup();
|
|
resolve(mode);
|
|
});
|
|
});
|
|
|
|
// Beim nächsten Klick außerhalb schließen
|
|
setTimeout(() => document.addEventListener('click', outsideClick), 10);
|
|
});
|
|
}
|
|
|
|
// ── Reply Button ──
|
|
|
|
function _addReplyButton(div) {
|
|
const btn = document.createElement('button');
|
|
btn.className = 'msg-reply-btn icon-btn';
|
|
btn.innerHTML = '<i class="fa fa-reply"></i>';
|
|
btn.title = 'Antworten';
|
|
btn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
// Nur den eigentlichen Nachrichten-Text holen (ohne Reply-Button-Text)
|
|
const clone = div.cloneNode(true);
|
|
clone.querySelectorAll('.msg-reply-btn, .attachment-indicator, .reply-quote').forEach(el => el.remove());
|
|
const text = clone.textContent.trim();
|
|
setReply({
|
|
role: div.dataset.role,
|
|
timestamp: div.dataset.timestamp || '',
|
|
text
|
|
});
|
|
});
|
|
div.prepend(btn);
|
|
}
|