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:
1 parent
7c0e5d180a
commit
57b4b5931b
6 files changed
+224
-6
No files matched your search
+67
-2
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user