KI-Chat: Verlaufs-Titel inline umbenennbar
Pro Verlauf ein Stift-Button (neben dem Löschen-Button). Klick macht den Titel inline editierbar; Enter/Blur speichert via PATCH /chat/api/threads/:id, Escape bricht ab. Auch für client-seitig neu angelegte Threads. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -145,6 +145,12 @@
|
||||
'<div class="truncate font-medium">' + escapeHtml(titel || '(neuer Chat)') + '</div>' +
|
||||
'<div class="text-[11px] text-gray-400">gerade</div>' +
|
||||
'</a>' +
|
||||
'<button type="button" data-rename-thread="' + id +
|
||||
'" class="threadRename shrink-0 p-1.5 rounded text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30 opacity-0 group-hover:opacity-100 focus:opacity-100" title="Namen ändern" aria-label="Namen ändern">' +
|
||||
'<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>' +
|
||||
'</svg>' +
|
||||
'</button>' +
|
||||
'<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">' +
|
||||
@@ -295,6 +301,69 @@
|
||||
newThreadBtn.addEventListener('click', resetToNewChat);
|
||||
}
|
||||
|
||||
// Rename a thread inline: replace the title with an input, save on
|
||||
// Enter/blur (PATCH), cancel on Escape.
|
||||
async function saveRename(input, link, id, original) {
|
||||
const val = input.value.trim();
|
||||
if (!val) { link.querySelector('.font-medium').textContent = original; return; }
|
||||
try {
|
||||
const res = await fetch('/chat/api/threads/' + id, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ titel: val }),
|
||||
});
|
||||
if (!res.ok) throw new Error('Speichern fehlgeschlagen');
|
||||
link.querySelector('.font-medium').textContent = val;
|
||||
} catch (err) {
|
||||
link.querySelector('.font-medium').textContent = original;
|
||||
alert(err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function startRename(li, id) {
|
||||
const link = li.querySelector('.threadLink');
|
||||
const titleEl = link.querySelector('.font-medium');
|
||||
const original = titleEl.textContent;
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = original;
|
||||
input.className = 'font-medium w-full bg-transparent border border-blue-400 rounded px-1 py-0.5 text-sm text-gray-800 dark:text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500';
|
||||
titleEl.replaceWith(input);
|
||||
input.focus();
|
||||
input.select();
|
||||
let done = false;
|
||||
const commit = () => {
|
||||
if (done) return; done = true;
|
||||
const title = document.createElement('div');
|
||||
title.className = 'truncate font-medium';
|
||||
title.textContent = input.value.trim() || original;
|
||||
input.replaceWith(title);
|
||||
if (input.value.trim() && input.value.trim() !== original) {
|
||||
saveRename(input, link, id, original);
|
||||
} else {
|
||||
title.textContent = original;
|
||||
}
|
||||
};
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') { e.preventDefault(); commit(); }
|
||||
else if (e.key === 'Escape') { e.preventDefault(); done = true; const title = document.createElement('div'); title.className = 'truncate font-medium'; title.textContent = original; input.replaceWith(title); }
|
||||
});
|
||||
input.addEventListener('blur', commit);
|
||||
}
|
||||
|
||||
if (threadList) {
|
||||
threadList.addEventListener('click', (e) => {
|
||||
const renameBtn = e.target.closest('[data-rename-thread]');
|
||||
if (renameBtn) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const id = Number(renameBtn.getAttribute('data-rename-thread'));
|
||||
const li = threadList.querySelector('li[data-thread-li="' + id + '"]');
|
||||
if (li) startRename(li, id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Delete a thread from the sidebar. Event delegation: works for server-rendered
|
||||
// rows and ones added client-side (prependThread).
|
||||
if (threadList) {
|
||||
|
||||
@@ -66,6 +66,13 @@
|
||||
<div class="truncate font-medium"><%= t.titel || '(ohne Titel)' %></div>
|
||||
<div class="text-[11px] text-gray-400"><%= new Date(t.updated_at).toLocaleDateString('de-DE') %></div>
|
||||
</a>
|
||||
<button type="button" data-rename-thread="<%= t.id %>"
|
||||
class="threadRename shrink-0 p-1.5 rounded text-gray-400 hover:text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30 opacity-0 group-hover:opacity-100 focus:opacity-100"
|
||||
title="Namen ändern" aria-label="Namen ändern">
|
||||
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user