KI-Chat: Verlauf wird beim Laden angezeigt + einzelne Chats löschbar
- initialMessages-JSON wurde mit <%= %> HTML-escaped, wodurch JSON.parse fehlschlug und die Nachrichtenfläche beim Öffnen eines Verlaufs leer blieb. Jetzt <%- %> mit < -> < escapet, JSON bleibt parsebar. - Pro Verlauf ein Trash-Button in der Sidebar (DELETE /chat/api/threads/:id). Löscht den Verlauf, entfernt die Zeile, setzt bei aktivem Verlauf auf neuen Chat zurück. Auch für client-seitig neu angelegte Threads. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+46
-4
@@ -137,10 +137,21 @@
|
|||||||
const empty = threadList.querySelector('li.text-center');
|
const empty = threadList.querySelector('li.text-center');
|
||||||
if (empty) empty.remove();
|
if (empty) empty.remove();
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.innerHTML = '<a href="/chat?thread=' + encodeURIComponent(id) + '" data-thread="' + id +
|
li.setAttribute('data-thread-li', id);
|
||||||
'" class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700/40">' +
|
li.innerHTML =
|
||||||
'<div class="truncate font-medium">' + escapeHtml(titel || '(neuer Chat)') + '</div>' +
|
'<div class="flex items-center group">' +
|
||||||
'<div class="text-[11px] text-gray-400">gerade</div></a>';
|
'<a href="/chat?thread=' + encodeURIComponent(id) + '" data-thread="' + id +
|
||||||
|
'" class="threadLink flex-1 min-w-0 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700/40">' +
|
||||||
|
'<div class="truncate font-medium">' + escapeHtml(titel || '(neuer Chat)') + '</div>' +
|
||||||
|
'<div class="text-[11px] text-gray-400">gerade</div>' +
|
||||||
|
'</a>' +
|
||||||
|
'<button type="button" data-delete-thread="' + id +
|
||||||
|
'" class="threadDelete shrink-0 mr-2 p-1.5 rounded text-gray-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 opacity-0 group-hover:opacity-100 focus:opacity-100" title="Verlauf löschen" aria-label="Verlauf löschen">' +
|
||||||
|
'<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">' +
|
||||||
|
'<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M1 7h22M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3"></path>' +
|
||||||
|
'</svg>' +
|
||||||
|
'</button>' +
|
||||||
|
'</div>';
|
||||||
threadList.insertBefore(li, threadList.firstChild);
|
threadList.insertBefore(li, threadList.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,6 +295,37 @@
|
|||||||
newThreadBtn.addEventListener('click', resetToNewChat);
|
newThreadBtn.addEventListener('click', resetToNewChat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete a thread from the sidebar. Event delegation: works for server-rendered
|
||||||
|
// rows and ones added client-side (prependThread).
|
||||||
|
if (threadList) {
|
||||||
|
threadList.addEventListener('click', async (e) => {
|
||||||
|
const btn = e.target.closest('[data-delete-thread]');
|
||||||
|
if (!btn) return;
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const id = Number(btn.getAttribute('data-delete-thread'));
|
||||||
|
if (!id || !confirm('Diesen Chat-Verlauf wirklich löschen?')) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/chat/api/threads/' + id, { method: 'DELETE' });
|
||||||
|
if (!res.ok) throw new Error('Löschen fehlgeschlagen (' + res.status + ')');
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const li = threadList.querySelector('li[data-thread-li="' + id + '"]');
|
||||||
|
if (li) li.remove();
|
||||||
|
// If the deleted thread was active, start a fresh chat.
|
||||||
|
if (activeId === id) resetToNewChat();
|
||||||
|
// If no threads remain, show the empty placeholder.
|
||||||
|
if (!threadList.querySelector('li[data-thread-li]')) {
|
||||||
|
const empty = document.createElement('li');
|
||||||
|
empty.className = 'px-3 py-4 text-sm text-gray-400 text-center';
|
||||||
|
empty.textContent = 'Noch keine Verläufe.';
|
||||||
|
threadList.appendChild(empty);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Render any pre-loaded messages (existing thread) with the same renderer.
|
// Render any pre-loaded messages (existing thread) with the same renderer.
|
||||||
function renderInitial() {
|
function renderInitial() {
|
||||||
const blob = document.getElementById('initialMessages');
|
const blob = document.getElementById('initialMessages');
|
||||||
|
|||||||
+17
-8
@@ -43,13 +43,22 @@
|
|||||||
<div class="px-3 py-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">Verläufe</div>
|
<div class="px-3 py-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400 border-b border-gray-200 dark:border-gray-700">Verläufe</div>
|
||||||
<ul id="threadList" class="overflow-y-auto flex-1 divide-y divide-gray-100 dark:divide-gray-700">
|
<ul id="threadList" class="overflow-y-auto flex-1 divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
<% threads.forEach(function(t) { %>
|
<% threads.forEach(function(t) { %>
|
||||||
<li>
|
<li data-thread-li="<%= t.id %>">
|
||||||
<a href="/chat?thread=<%= t.id %>"
|
<div class="flex items-center group <%= (t.id === activeId) ? 'bg-blue-50 dark:bg-gray-700' : '' %>">
|
||||||
data-thread="<%= t.id %>"
|
<a href="/chat?thread=<%= t.id %>"
|
||||||
class="threadLink block px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700/40 <%= (t.id === activeId) ? 'bg-blue-50 dark:bg-gray-700' : '' %>">
|
data-thread="<%= t.id %>"
|
||||||
<div class="truncate font-medium"><%= t.titel || '(ohne Titel)' %></div>
|
class="threadLink flex-1 min-w-0 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700/40">
|
||||||
<div class="text-[11px] text-gray-400"><%= new Date(t.updated_at).toLocaleDateString('de-DE') %></div>
|
<div class="truncate font-medium"><%= t.titel || '(ohne Titel)' %></div>
|
||||||
</a>
|
<div class="text-[11px] text-gray-400"><%= new Date(t.updated_at).toLocaleDateString('de-DE') %></div>
|
||||||
|
</a>
|
||||||
|
<button type="button" data-delete-thread="<%= t.id %>"
|
||||||
|
class="threadDelete shrink-0 mr-2 p-1.5 rounded text-gray-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/30 opacity-0 group-hover:opacity-100 focus:opacity-100"
|
||||||
|
title="Verlauf löschen" aria-label="Verlauf löschen">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M1 7h22M9 7V4a1 1 0 011-1h4a1 1 0 011 1v3"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
<% if (!threads.length) { %>
|
<% if (!threads.length) { %>
|
||||||
@@ -80,7 +89,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script id="initialMessages" type="application/json"><%= JSON.stringify(messages.map(function(m){ return { role: m.role, content: m.content }; })) %></script>
|
<script id="initialMessages" type="application/json"><%- JSON.stringify(messages.map(function(m){ return { role: m.role, content: m.content }; })).replace(/</g, '\\u003c') %></script>
|
||||||
<input type="hidden" id="activeThread" value="<%= activeId || '' %>">
|
<input type="hidden" id="activeThread" value="<%= activeId || '' %>">
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user