feat(frontend): add inline session title rename in sidebar
Allow users to rename chat sessions from the sidebar menu with inline editing, API persistence, and i18n feedback.
This commit is contained in:
@@ -26,6 +26,10 @@ export async function generateSessionsTitle(session_id: string, data: any) {
|
||||
return post(`/api/v1/sessions/${session_id}/generate_title`, data);
|
||||
}
|
||||
|
||||
export async function updateSession(session_id: string, data: { title: string; description?: string }) {
|
||||
return put(`/api/v1/sessions/${session_id}`, data);
|
||||
}
|
||||
|
||||
export async function knowledgeChat(data: { session_id: string; query: string; }) {
|
||||
return postChat(`/api/v1/knowledge-chat/${data.session_id}`, { query: data.query, channel: "web" });
|
||||
}
|
||||
@@ -75,4 +79,4 @@ export async function stopSession(session_id: string, message_id: string) {
|
||||
|
||||
export async function clearSessionMessages(session_id: string) {
|
||||
return del(`/api/v1/sessions/${session_id}/messages`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,22 @@
|
||||
@click.stop
|
||||
@change="emit('toggle-select')"
|
||||
/>
|
||||
<span class="submenu_title" :class="batchMode ? 'submenu_title--batch' : ''" :title="item.title">
|
||||
<form
|
||||
v-if="titleEditing"
|
||||
class="session-title-edit"
|
||||
@submit.prevent="submitTitleEdit"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
ref="titleInputRef"
|
||||
v-model="titleDraft"
|
||||
class="session-title-edit__input"
|
||||
:maxlength="SESSION_TITLE_MAX_LENGTH"
|
||||
@keydown.esc.prevent="cancelTitleEdit"
|
||||
@blur="submitTitleEdit"
|
||||
/>
|
||||
</form>
|
||||
<span v-else class="submenu_title" :class="batchMode ? 'submenu_title--batch' : ''" :title="item.title">
|
||||
<t-icon v-if="item.is_pinned" name="pin" class="submenu_pin_icon" />
|
||||
<span class="submenu_title-text">{{ item.title }}</span>
|
||||
</span>
|
||||
@@ -64,6 +79,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, nextTick, ref } from 'vue'
|
||||
import { normalizeSessionTitleDraft, SESSION_TITLE_MAX_LENGTH } from './sessionTitleEdit'
|
||||
|
||||
interface SessionMenuOption {
|
||||
content: string
|
||||
@@ -72,7 +88,7 @@ interface SessionMenuOption {
|
||||
prefixIcon?: any
|
||||
}
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
item: { id: string; path: string; title: string; is_pinned?: boolean }
|
||||
batchMode: boolean
|
||||
activePath: string
|
||||
@@ -86,6 +102,7 @@ const emit = defineEmits<{
|
||||
(e: 'navigate'): void
|
||||
(e: 'toggle-select'): void
|
||||
(e: 'menu-click', data: { value: string }): void
|
||||
(e: 'rename-submit', data: { title: string }): void
|
||||
(e: 'hover-in'): void
|
||||
(e: 'hover-out'): void
|
||||
}>()
|
||||
@@ -95,7 +112,10 @@ const MENU_GAP = 4
|
||||
const VIEWPORT_MARGIN = 8
|
||||
|
||||
const menuOpen = ref(false)
|
||||
const titleEditing = ref(false)
|
||||
const titleDraft = ref('')
|
||||
const triggerRef = ref<HTMLButtonElement | null>(null)
|
||||
const titleInputRef = ref<HTMLInputElement | null>(null)
|
||||
const menuStyle = ref<Record<string, string>>({})
|
||||
|
||||
const updateMenuPosition = (): void => {
|
||||
@@ -138,7 +158,36 @@ const toggleMenu = (): void => {
|
||||
})
|
||||
}
|
||||
|
||||
const startTitleEdit = (): void => {
|
||||
closeMenu()
|
||||
titleDraft.value = props.item.title || ''
|
||||
titleEditing.value = true
|
||||
nextTick(() => {
|
||||
titleInputRef.value?.focus()
|
||||
titleInputRef.value?.select()
|
||||
})
|
||||
}
|
||||
|
||||
const cancelTitleEdit = (): void => {
|
||||
titleEditing.value = false
|
||||
titleDraft.value = ''
|
||||
}
|
||||
|
||||
const submitTitleEdit = (): void => {
|
||||
if (!titleEditing.value) return
|
||||
const nextTitle = normalizeSessionTitleDraft(titleDraft.value)
|
||||
const currentTitle = normalizeSessionTitleDraft(props.item.title || '')
|
||||
titleEditing.value = false
|
||||
titleDraft.value = ''
|
||||
if (!nextTitle || nextTitle === currentTitle) return
|
||||
emit('rename-submit', { title: nextTitle })
|
||||
}
|
||||
|
||||
const handleMenuClick = (option: SessionMenuOption): void => {
|
||||
if (option.value === 'rename') {
|
||||
startTitleEdit()
|
||||
return
|
||||
}
|
||||
closeMenu()
|
||||
emit('menu-click', { value: option.value })
|
||||
}
|
||||
@@ -158,6 +207,25 @@ onBeforeUnmount(() => {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.session-title-edit {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.session-title-edit__input {
|
||||
width: 100%;
|
||||
height: 26px;
|
||||
padding: 0 8px;
|
||||
border: 1px solid var(--td-brand-color);
|
||||
border-radius: 5px;
|
||||
color: var(--td-text-color-primary);
|
||||
background: var(--td-bg-color-container);
|
||||
font-size: 14px;
|
||||
line-height: 24px;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px var(--td-brand-color-light);
|
||||
}
|
||||
|
||||
.menu-more-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
@navigate="gotopage(subitem.path)"
|
||||
@toggle-select="toggleBatchSelect(subitem.id)"
|
||||
@menu-click="handleSessionMenuClick($event, subitem)"
|
||||
@rename-submit="renameSessionTitle(subitem, $event.title)"
|
||||
@hover-in="mouseenteBotDownr(subitem.id)" @hover-out="mouseleaveBotDown" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -201,7 +202,7 @@
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { onMounted, onUnmounted, watch, computed, ref, h, nextTick } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { getSessionsList, delSession, batchDelSessions, deleteAllSessions, clearSessionMessages, pinSession, unpinSession } from "@/api/chat/index";
|
||||
import { getSessionsList, delSession, batchDelSessions, deleteAllSessions, clearSessionMessages, pinSession, unpinSession, updateSession } from "@/api/chat/index";
|
||||
import { useChatResourcesStore } from '@/stores/chatResources';
|
||||
import { listAllIMChannels } from '@/api/agent/index';
|
||||
import SessionSidebarRow from './SessionSidebarRow.vue';
|
||||
@@ -616,6 +617,7 @@ const buildSessionMenuOptions = (item: any) => {
|
||||
});
|
||||
}
|
||||
options.push(
|
||||
{ content: t('menu.renameSession'), value: 'rename', prefixIcon: () => h(TIcon, { name: 'edit-1', size: '16px' }) },
|
||||
{ content: t('menu.clearMessages'), value: 'clearMessages', prefixIcon: () => h(TIcon, { name: 'clear', size: '16px' }) },
|
||||
{ content: t('menu.batchManage'), value: 'batchManage', prefixIcon: () => h(TIcon, { name: 'queue', size: '16px' }) },
|
||||
{ content: t('upload.deleteRecord'), value: 'delete', theme: 'error', prefixIcon: () => h(TIcon, { name: 'delete', size: '16px' }) },
|
||||
@@ -638,6 +640,26 @@ const updateSessionInBuckets = (
|
||||
syncMenuStoreFromBuckets();
|
||||
};
|
||||
|
||||
const renameSessionTitle = async (item: any, title: string) => {
|
||||
try {
|
||||
const res: any = await updateSession(item.id, {
|
||||
title,
|
||||
description: item.description || '',
|
||||
});
|
||||
if (res && res.success) {
|
||||
updateSessionInBuckets(item.id, {
|
||||
title: res.data?.title || title,
|
||||
isNoTitle: false,
|
||||
});
|
||||
MessagePlugin.success(t('menu.renameSessionSuccess'));
|
||||
} else {
|
||||
MessagePlugin.error(t('menu.renameSessionFailed'));
|
||||
}
|
||||
} catch {
|
||||
MessagePlugin.error(t('menu.renameSessionFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const togglePin = (item: any, pin: boolean) => {
|
||||
if (pinningIds.value.has(item.id)) return;
|
||||
pinningIds.value.add(item.id);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import { normalizeSessionTitleDraft } from './sessionTitleEdit.ts'
|
||||
|
||||
test('normalizeSessionTitleDraft trims whitespace and collapses inner runs', () => {
|
||||
assert.equal(normalizeSessionTitleDraft(' Quarterly summary\t review '), 'Quarterly summary review')
|
||||
})
|
||||
|
||||
test('normalizeSessionTitleDraft rejects blank titles after trimming', () => {
|
||||
assert.equal(normalizeSessionTitleDraft(' \n\t '), '')
|
||||
})
|
||||
|
||||
test('normalizeSessionTitleDraft caps long titles at 80 characters', () => {
|
||||
const input = 'x'.repeat(90)
|
||||
assert.equal(normalizeSessionTitleDraft(input), 'x'.repeat(80))
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
export const SESSION_TITLE_MAX_LENGTH = 80
|
||||
|
||||
export function normalizeSessionTitleDraft(value: string): string {
|
||||
return value.trim().replace(/\s+/g, ' ').slice(0, SESSION_TITLE_MAX_LENGTH)
|
||||
}
|
||||
@@ -15,6 +15,9 @@ export default {
|
||||
clearMessages: 'Clear Messages',
|
||||
clearMessagesSuccess: 'Messages cleared',
|
||||
clearMessagesFailed: 'Failed to clear messages, please try again later',
|
||||
renameSession: 'Rename',
|
||||
renameSessionSuccess: 'Title updated',
|
||||
renameSessionFailed: 'Failed to update title, please try again later',
|
||||
batchManage: 'Batch Manage',
|
||||
newSession: 'New Chat',
|
||||
pin: 'Pin',
|
||||
|
||||
@@ -15,6 +15,9 @@ export default {
|
||||
clearMessages: "메시지 지우기",
|
||||
clearMessagesSuccess: "메시지가 지워졌습니다",
|
||||
clearMessagesFailed: "메시지 지우기 실패, 나중에 다시 시도해 주세요",
|
||||
renameSession: "제목 수정",
|
||||
renameSessionSuccess: "제목이 업데이트되었습니다",
|
||||
renameSessionFailed: "제목 수정 실패, 나중에 다시 시도해 주세요",
|
||||
batchManage: "일괄 관리",
|
||||
newSession: "새 세션",
|
||||
pin: "고정",
|
||||
|
||||
@@ -12,6 +12,9 @@ export default {
|
||||
clearMessages: 'Очистить сообщения',
|
||||
clearMessagesSuccess: 'Сообщения очищены',
|
||||
clearMessagesFailed: 'Не удалось очистить сообщения, попробуйте позже',
|
||||
renameSession: 'Переименовать',
|
||||
renameSessionSuccess: 'Название обновлено',
|
||||
renameSessionFailed: 'Не удалось обновить название, попробуйте позже',
|
||||
batchManage: 'Пакетное управление',
|
||||
newSession: 'Новый диалог',
|
||||
pin: 'Закрепить',
|
||||
|
||||
@@ -15,6 +15,9 @@ export default {
|
||||
clearMessages: "清空消息",
|
||||
clearMessagesSuccess: "消息已清空",
|
||||
clearMessagesFailed: "清空消息失败,请稍后再试",
|
||||
renameSession: "修改标题",
|
||||
renameSessionSuccess: "标题已更新",
|
||||
renameSessionFailed: "修改标题失败,请稍后再试",
|
||||
batchManage: "批量管理",
|
||||
newSession: "新会话",
|
||||
pin: "置顶",
|
||||
|
||||
Reference in New Issue
Block a user