feat(web): support manually renaming sessions in console #2897

This commit is contained in:
zhayujie
2026-06-16 20:03:38 +08:00
parent 35e42a3ad6
commit 3bb8ec3bea
2 changed files with 132 additions and 0 deletions

View File

@@ -244,6 +244,52 @@
}
.dark .session-delete:hover { background: rgba(239, 68, 68, 0.15); }
/* Rename button: shares the look of the delete button, sits to its left.
Negative right margin tightens the gap to the delete button only. */
.session-rename {
flex-shrink: 0;
margin-right: -6px;
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
color: #9ca3af;
font-size: 11px;
opacity: 0;
transition: opacity 0.15s, color 0.15s, background 0.15s;
cursor: pointer;
background: none;
border: none;
padding: 0;
}
.session-item:hover .session-rename { opacity: 1; }
.session-rename:hover {
color: #4ABE6E;
background: rgba(74, 190, 110, 0.12);
}
.dark .session-rename:hover { background: rgba(74, 190, 110, 0.18); }
/* Inline title editor */
.session-title-input {
flex: 1;
min-width: 0;
font-size: 13px;
font-family: inherit;
color: #111827;
background: #ffffff;
border: 1px solid #4ABE6E;
border-radius: 6px;
padding: 2px 6px;
outline: none;
}
.dark .session-title-input {
color: #e5e5e5;
background: rgba(255, 255, 255, 0.06);
border-color: #4ABE6E;
}
/* Context Divider */
.context-divider {
display: flex;

View File

@@ -220,6 +220,7 @@ const I18N = {
today: '今天', yesterday: '昨天', earlier: '更早',
delete_session_confirm: '确认删除该会话?所有消息将被清除。',
delete_session_title: '删除会话',
rename_session: '重命名',
delete_message_confirm: '确认删除这条消息?',
delete_message_title: '删除消息',
edit_disabled_reply_active: '正在生成回复,暂时无法编辑。',
@@ -460,6 +461,7 @@ const I18N = {
today: 'Today', yesterday: 'Yesterday', earlier: 'Earlier',
delete_session_confirm: 'Delete this session? All messages will be removed.',
delete_session_title: 'Delete Session',
rename_session: 'Rename',
delete_message_confirm: 'Delete this message?',
delete_message_title: 'Delete Message',
edit_disabled_reply_active: 'Reply is being generated; editing is temporarily unavailable.',
@@ -3670,6 +3672,9 @@ function _addOptimisticSessionItem(sid) {
item.innerHTML = `
<i class="fas fa-message session-icon"></i>
<span class="session-title" title="${escapeHtml(title)}">${escapeHtml(title)}</span>
<button class="session-rename" onclick="event.stopPropagation(); renameSession('${sid}')" title="${escapeHtml(t('rename_session'))}">
<i class="fas fa-pen"></i>
</button>
<button class="session-delete" onclick="event.stopPropagation(); deleteSession('${sid}')" title="Delete">
<i class="fas fa-trash-can"></i>
</button>
@@ -3757,6 +3762,9 @@ function _fetchSessionPage(page, clear, onDone) {
item.innerHTML = `
<i class="fas fa-message session-icon"></i>
<span class="session-title" title="${escapeHtml(title)}">${escapeHtml(title)}</span>
<button class="session-rename" onclick="event.stopPropagation(); renameSession('${s.session_id}')" title="${escapeHtml(t('rename_session'))}">
<i class="fas fa-pen"></i>
</button>
<button class="session-delete" onclick="event.stopPropagation(); deleteSession('${s.session_id}')" title="Delete">
<i class="fas fa-trash-can"></i>
</button>
@@ -3875,6 +3883,84 @@ function switchSession(newSessionId) {
if (currentView !== 'chat') navigateTo('chat');
}
// In-place rename a session title: replace the title <span> with an <input>,
// commit on Enter/blur, cancel on Escape. Persists via PUT /api/sessions/<id>.
function renameSession(sid) {
const item = document.querySelector(`.session-item[data-session-id="${sid}"]`);
if (!item) return;
const titleEl = item.querySelector('.session-title');
if (!titleEl || item.querySelector('.session-title-input')) return;
const oldTitle = titleEl.textContent;
const input = document.createElement('input');
input.type = 'text';
input.className = 'session-title-input';
input.value = oldTitle;
input.maxLength = 100;
// Avoid switching session while interacting with the input
const stop = e => e.stopPropagation();
input.addEventListener('click', stop);
input.addEventListener('mousedown', stop);
titleEl.replaceWith(input);
input.focus();
input.select();
let done = false;
const restore = (title) => {
if (done) return;
done = true;
const span = document.createElement('span');
span.className = 'session-title';
span.title = title;
span.textContent = title;
input.replaceWith(span);
};
const commit = () => {
if (done) return;
const newTitle = input.value.trim();
if (!newTitle || newTitle === oldTitle) {
restore(oldTitle);
return;
}
// Optimistically show the new title, then persist.
restore(newTitle);
fetch(`/api/sessions/${encodeURIComponent(sid)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTitle })
})
.then(r => r.json())
.then(data => {
if (data.status !== 'success') {
// Revert UI on failure
const span = item.querySelector('.session-title');
if (span) {
span.title = oldTitle;
span.textContent = oldTitle;
}
}
})
.catch(() => {
const span = item.querySelector('.session-title');
if (span) {
span.title = oldTitle;
span.textContent = oldTitle;
}
});
};
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); commit(); }
else if (e.key === 'Escape') { e.preventDefault(); restore(oldTitle); }
});
input.addEventListener('blur', commit);
}
function deleteSession(sid) {
showConfirmModal(t('delete_session_title'), t('delete_session_confirm'), () => {
// Before deleting, find the next real session to fall back to when the