diff --git a/channel/web/static/css/console.css b/channel/web/static/css/console.css index a618265d..2283f3e8 100644 --- a/channel/web/static/css/console.css +++ b/channel/web/static/css/console.css @@ -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; diff --git a/channel/web/static/js/console.js b/channel/web/static/js/console.js index f5283e96..b649951d 100644 --- a/channel/web/static/js/console.js +++ b/channel/web/static/js/console.js @@ -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 = ` ${escapeHtml(title)} + @@ -3757,6 +3762,9 @@ function _fetchSessionPage(page, clear, onDone) { item.innerHTML = ` ${escapeHtml(title)} + @@ -3875,6 +3883,84 @@ function switchSession(newSessionId) { if (currentView !== 'chat') navigateTo('chat'); } +// In-place rename a session title: replace the title with an , +// commit on Enter/blur, cancel on Escape. Persists via PUT /api/sessions/. +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