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,208 @@
|
||||
// ── 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);
|
||||
}
|
||||
Reference in new issue
Block a user