feat: Reply/Zitat-Feature für Chat-Nachrichten

- Reply-Button (Hover) auf jeder User/Assistant-Nachricht
- Reply-Preview-Leiste über dem Input-Feld (mit Timestamp, Rolle, Text-Preview)
- Beim Senden: Zitat als Kontext-Block vorangestellt (> [Zitat: HH:MM, Role]:)
- User-Nachrichten parsen Zitat-Blöcke als visuelle Quote-Elemente (blaue Borderlinie)
- Timestamp-basierte Referenzierung (HH:MM oder DD.MM. HH:MM)
- clearReply() bei Chat-Clear, Navigation und Destroy
This commit is contained in:
Meleki committed 2026-03-01 01:06:23 +01:00
1 parent 7c0e5d180a
commit 57b4b5931b
6 files changed
+224 -6

No files matched your search

+62 -2
View File
@@ -33,10 +33,64 @@ function truncate(s, max) {
return s && s.length > max ? s.substring(0, max) + '...' : (s || '');
}
// ── 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.appendChild(btn);
}
// ── Message Bubbles ──
function renderUserContent(container, text) {
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);
}
return;
}
_renderUserTextContent(container, raw);
}
function _renderUserTextContent(container, raw) {
// [Datei: path] oder [Datei: path:1-10] erkennen
const pattern = /\[Datei: ([^\]]+)\]/g;
let last = 0;
@@ -75,19 +129,25 @@ function renderUserContent(container, text) {
container.appendChild(frag);
}
function addUserBubble(text, streamed = false) {
function addUserBubble(text, streamed = false, timestamp = '') {
const div = document.createElement('div');
div.className = 'msg user';
div.dataset.role = 'user';
div.dataset.timestamp = timestamp || new Date().toISOString();
if (streamed) div.classList.add('streamed');
renderUserContent(div, text);
_addReplyButton(div);
chatEl().appendChild(div);
scrollDown();
}
function addAssistantBubble(text) {
function addAssistantBubble(text, timestamp = '') {
const div = document.createElement('div');
div.className = 'msg assistant markdown';
div.dataset.role = 'assistant';
div.dataset.timestamp = timestamp || new Date().toISOString();
div.innerHTML = renderMarkdown(text);
_addReplyButton(div);
chatEl().appendChild(div);
scrollDown();
return div;