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

+67 -2
View File
@@ -32,6 +32,7 @@ const messages = [];
let isStreaming = false;
let pendingFiles = []; // [{ name, type, base64 }]
let pendingRefs = []; // [{ path, from, to }] - Workspace-Dateireferenzen
let pendingReply = null; // { index, role, text } - Zitierte Nachricht
let _lastDoneChainData = null; // Chain-Daten vom letzten done-Event
let _chatAbandoned = false; // true wenn User weg-navigiert während Stream läuft
@@ -43,6 +44,7 @@ async function clearChat() {
chatEl().innerHTML = '';
lastKnownMessageId = 0;
loadedArchiveCount = 0;
clearReply();
setMeta(`Archiviert: ${archive.chainCount} Chains, ${archive.messageCount} Messages`);
showArchiveLoadButton();
@@ -60,14 +62,24 @@ async function sendMessage() {
let content;
let bubbleText = text;
// Refs in den Text einbauen
// Reply-Zitat einbauen
let msgText = text;
if (pendingReply) {
const roleLabel = pendingReply.role === 'user' ? 'User' : 'Assistant';
const timeLabel = _formatReplyTimestamp(pendingReply.timestamp);
const quoteText = pendingReply.text.split('\n').map(l => '> ' + l).join('\n');
const quoteHeader = `> [Zitat: ${timeLabel}, ${roleLabel}]:`;
msgText = quoteHeader + '\n' + quoteText + '\n\n' + (text || '');
clearReply();
}
// Refs in den Text einbauen
if (pendingRefs.length > 0) {
const refTags = pendingRefs.map(r => {
if (r.from && r.to) return `[Datei: ${r.path}:${r.from}-${r.to}]`;
return `[Datei: ${r.path}]`;
});
msgText = refTags.join('\n') + (text ? '\n' + text : '');
msgText = refTags.join('\n') + (msgText ? '\n' + msgText : '');
clearPendingRefs();
}
@@ -324,3 +336,56 @@ function clearPendingFiles() {
const fileInput = $('#fileInput');
if (fileInput) fileInput.value = '';
}
// ── Reply / Quote ──
function _formatReplyTimestamp(ts) {
if (!ts) return 'Jetzt';
try {
const d = new Date(ts);
const now = new Date();
const hh = d.getHours().toString().padStart(2, '0');
const mm = d.getMinutes().toString().padStart(2, '0');
// Gleiches Datum → nur Uhrzeit, sonst Datum + Uhrzeit
if (d.toDateString() === now.toDateString()) {
return hh + ':' + mm;
}
const dd = d.getDate().toString().padStart(2, '0');
const mo = (d.getMonth() + 1).toString().padStart(2, '0');
return dd + '.' + mo + '. ' + hh + ':' + mm;
} catch { return 'Jetzt'; }
}
function setReply(reply) {
pendingReply = reply;
renderReplyPreview();
document.getElementById('input')?.focus();
}
function clearReply() {
pendingReply = null;
renderReplyPreview();
}
function renderReplyPreview() {
const container = $('#replyPreview');
if (!container) return;
if (!pendingReply) {
container.innerHTML = '';
container.hidden = true;
return;
}
const roleLabel = pendingReply.role === 'user' ? 'User' : 'Assistant';
const timeLabel = _formatReplyTimestamp(pendingReply.timestamp);
const preview = truncate(pendingReply.text, 150);
container.innerHTML = `<div class="reply-preview-item">
<span class="reply-icon"><i class="fa fa-reply"></i></span>
<span class="reply-meta">${esc(timeLabel)}, ${esc(roleLabel)}</span>
<span class="reply-text">${esc(preview)}</span>
<button class="file-remove" onclick="clearReply()"><i class="fa fa-times"></i></button>
</div>`;
container.hidden = false;
}