From 1ce1fc53d3b703bac78c0b8231c1172ee51c3d02 Mon Sep 17 00:00:00 2001 From: Justin3go Date: Mon, 15 Jan 2024 11:16:35 +0800 Subject: [PATCH 01/20] =?UTF-8?q?=E2=9C=A8=20feat:=20implement=20custom=20?= =?UTF-8?q?session=20grouping=20(#1045)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SessionListContent/DefaultMode.tsx | 11 ++- .../SessionListContent/List/Item/Actions.tsx | 75 ++++++++++++++--- .../List/Item/CreateGroupModal.tsx | 45 ++++++++++ .../SessionListContent/List/Item/index.tsx | 84 ++++++++++++------- src/locales/default/chat.ts | 1 + src/locales/default/common.ts | 7 ++ src/store/session/slices/session/action.ts | 7 ++ .../session/slices/session/selectors/list.ts | 17 ++++ 8 files changed, 204 insertions(+), 43 deletions(-) create mode 100644 src/app/chat/features/SessionListContent/List/Item/CreateGroupModal.tsx diff --git a/src/app/chat/features/SessionListContent/DefaultMode.tsx b/src/app/chat/features/SessionListContent/DefaultMode.tsx index 1d3302dd6337..a1323102e616 100644 --- a/src/app/chat/features/SessionListContent/DefaultMode.tsx +++ b/src/app/chat/features/SessionListContent/DefaultMode.tsx @@ -16,6 +16,8 @@ const SessionListContent = memo(() => { const { t } = useTranslation('chat'); const unpinnedSessionList = useSessionStore(sessionSelectors.unpinnedSessionList, isEqual); const pinnedList = useSessionStore(sessionSelectors.pinnedSessionList, isEqual); + const customSessionGroup = useSessionStore(sessionSelectors.customSessionGroup, isEqual); + const [hasPinnedSessionList, useFetchSessions] = useSessionStore((s) => [ sessionSelectors.hasPinnedSessionList(s), s.useFetchSessions, @@ -34,10 +36,15 @@ const SessionListContent = memo(() => { key: 'pinned', label: t('pin'), }, + ...Object.keys(customSessionGroup).map((key) => ({ + children: , + key, + label: key, + })), { children: , - key: 'sessionList', - label: t('sessionList'), + key: 'defaultList', + label: t('defaultList'), }, ].filter(Boolean) as CollapseProps['items']; diff --git a/src/app/chat/features/SessionListContent/List/Item/Actions.tsx b/src/app/chat/features/SessionListContent/List/Item/Actions.tsx index 0ef743ecd505..005e570c88e1 100644 --- a/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +++ b/src/app/chat/features/SessionListContent/List/Item/Actions.tsx @@ -1,7 +1,16 @@ import { ActionIcon, Icon } from '@lobehub/ui'; import { App, Dropdown, type MenuProps } from 'antd'; import { createStyles } from 'antd-style'; -import { HardDriveDownload, LucideCopy, MoreVertical, Pin, PinOff, Trash } from 'lucide-react'; +import isEqual from 'fast-deep-equal'; +import { + ArrowLeftRight, + HardDriveDownload, + LucideCopy, + MoreVertical, + Pin, + PinOff, + Trash, +} from 'lucide-react'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,6 +18,7 @@ import { configService } from '@/services/config'; import { useSessionStore } from '@/store/session'; import { sessionHelpers } from '@/store/session/helpers'; import { sessionSelectors } from '@/store/session/selectors'; +import { SessionGroupDefaultKeys } from '@/types/session'; const useStyles = createStyles(({ css }) => ({ modalRoot: css` @@ -17,27 +27,36 @@ const useStyles = createStyles(({ css }) => ({ })); interface ActionProps { + group: string | undefined; id: string; + setIsModalOpen: (open: boolean) => void; setOpen: (open: boolean) => void; } -const Actions = memo(({ id, setOpen }) => { +const Actions = memo(({ group, id, setIsModalOpen, setOpen }) => { const { t } = useTranslation('common'); const { styles } = useStyles(); - const [pin, removeSession, pinSession, duplicateSession] = useSessionStore((s) => { - const session = sessionSelectors.getSessionById(id)(s); - return [ - sessionHelpers.getSessionPinned(session), - s.removeSession, - s.pinSession, - s.duplicateSession, - ]; - }); + const customSessionGroup = useSessionStore(sessionSelectors.customSessionGroup, isEqual); + const [pin, removeSession, pinSession, duplicateSession, updateSessionGroup] = useSessionStore( + (s) => { + const session = sessionSelectors.getSessionById(id)(s); + return [ + sessionHelpers.getSessionPinned(session), + s.removeSession, + s.pinSession, + s.duplicateSession, + s.updateSessionGroup, + ]; + }, + ); const { modal } = App.useApp(); + const isDefault = group === SessionGroupDefaultKeys.Default; + const hasDivider = !isDefault || Object.keys(customSessionGroup).length > 0; + const items: MenuProps['items'] = useMemo( () => [ { @@ -48,6 +67,40 @@ const Actions = memo(({ id, setOpen }) => { pinSession(id, !pin); }, }, + { + children: [ + { + key: 'newGroup', + label:
{t('group.newGroup')}
, + onClick: ({ domEvent }) => { + domEvent.stopPropagation(); + setIsModalOpen(true); + }, + }, + hasDivider && { + type: 'divider', + }, + !isDefault && { + key: 'defaultList', + label:
{t('defaultList')}
, + onClick: () => { + updateSessionGroup(id, SessionGroupDefaultKeys.Default); + }, + }, + ...Object.keys(customSessionGroup) + .filter((key) => key !== group) + .map((key) => ({ + key, + label:
{key}
, + onClick: () => { + updateSessionGroup(id, key); + }, + })), + ], + icon: , + key: 'moveGroup', + label: t('group.moveGroup'), + }, { children: [ { diff --git a/src/app/chat/features/SessionListContent/List/Item/CreateGroupModal.tsx b/src/app/chat/features/SessionListContent/List/Item/CreateGroupModal.tsx new file mode 100644 index 000000000000..0bb1eadd1ec6 --- /dev/null +++ b/src/app/chat/features/SessionListContent/List/Item/CreateGroupModal.tsx @@ -0,0 +1,45 @@ +import { Input, Modal, type ModalProps } from '@lobehub/ui'; +import { message } from 'antd'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +type CreateGroupModalProps = ModalProps & { + onInputOk: (input: string) => void; +}; + +const CreateGroupModal = ({ open, onCancel, onInputOk }: CreateGroupModalProps) => { + const { t } = useTranslation('common'); + + const [input, setInput] = useState(''); + const [messageApi, contextHolder] = message.useMessage(); + + const handleClickOk = (input: string) => { + if (input.length === 0 || input.length > 10) { + messageApi.warning(t('group.inputMsg')); + return; + } + onInputOk(input); + }; + + return ( +
e.stopPropagation()}> + handleClickOk(input)} + open={open} + title={t('group.newGroup')} + width={400} + > + setInput(e.target.value)} + placeholder={t('group.inputPlaceholder')} + value={input} + /> + + {contextHolder} +
+ ); +}; + +export default CreateGroupModal; diff --git a/src/app/chat/features/SessionListContent/List/Item/index.tsx b/src/app/chat/features/SessionListContent/List/Item/index.tsx index 6b42b94e63d1..1dac61d92aee 100644 --- a/src/app/chat/features/SessionListContent/List/Item/index.tsx +++ b/src/app/chat/features/SessionListContent/List/Item/index.tsx @@ -13,6 +13,7 @@ import { agentSelectors, sessionSelectors } from '@/store/session/selectors'; import ListItem from '../../ListItem'; import Actions from './Actions'; +import CreateGroupModal from './CreateGroupModal'; interface SessionItemProps { id: string; @@ -20,32 +21,48 @@ interface SessionItemProps { const SessionItem = memo(({ id }) => { const [open, setOpen] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); const [defaultModel] = useGlobalStore((s) => [settingsSelectors.defaultAgentConfig(s).model]); const [active] = useSessionStore((s) => [s.activeId === id]); const [loading] = useChatStore((s) => [!!s.chatLoadingId && id === s.activeId]); - const [pin, title, description, systemRole, avatar, avatarBackground, updateAt, model] = - useSessionStore((s) => { - const session = sessionSelectors.getSessionById(id)(s); - const meta = session.meta; - const systemRole = session.config.systemRole; + const [ + pin, + title, + description, + systemRole, + avatar, + avatarBackground, + updateAt, + model, + group, + updateSessionGroup, + ] = useSessionStore((s) => { + const session = sessionSelectors.getSessionById(id)(s); + const meta = session.meta; + const systemRole = session.config.systemRole; - return [ - sessionHelpers.getSessionPinned(session), - agentSelectors.getTitle(meta), - agentSelectors.getDescription(meta), - systemRole, - agentSelectors.getAvatar(meta), - meta.backgroundColor, - session?.updatedAt, - session.config.model, - ]; - }); + return [ + sessionHelpers.getSessionPinned(session), + agentSelectors.getTitle(meta), + agentSelectors.getDescription(meta), + systemRole, + agentSelectors.getAvatar(meta), + meta.backgroundColor, + session?.updatedAt, + session.config.model, + session?.group, + s.updateSessionGroup, + ]; + }); const showModel = model !== defaultModel; - const actions = useMemo(() => , [id]); + const actions = useMemo( + () => , + [id], + ); const addon = useMemo( () => @@ -58,19 +75,26 @@ const SessionItem = memo(({ id }) => { ); return ( - + <> + + setIsModalOpen(false)} + onInputOk={(input) => updateSessionGroup(id, input)} + open={isModalOpen} + > + ); }, shallow); diff --git a/src/locales/default/chat.ts b/src/locales/default/chat.ts index 7443ceb8cb6f..1388ad8d2c3c 100644 --- a/src/locales/default/chat.ts +++ b/src/locales/default/chat.ts @@ -7,6 +7,7 @@ export default { confirmClearCurrentMessages: '即将清空当前会话消息,清空后将无法找回,请确认你的操作', confirmRemoveSessionItemAlert: '即将删除该助手,删除后该将无法找回,请确认你的操作', defaultAgent: '自定义助手', + defaultList: '默认列表', defaultSession: '自定义助手', duplicateTitle: '{{title}} 副本', historyRange: '历史范围', diff --git a/src/locales/default/common.ts b/src/locales/default/common.ts index cadde0f0ff4a..858a474df13d 100644 --- a/src/locales/default/common.ts +++ b/src/locales/default/common.ts @@ -16,6 +16,7 @@ export default { copyFail: '复制失败', copySuccess: '复制成功', defaultAgent: '自定义助手', + defaultList: '默认列表', defaultSession: '自定义助手', delete: '删除', duplicate: '创建副本', @@ -30,6 +31,12 @@ export default { globalSetting: '导出全局设置', }, feedback: '反馈与建议', + group: { + inputMsg: '组名长度需在1-10之内', + inputPlaceholder: '新建分组/移入指定分组', + moveGroup: '移入分组', + newGroup: '新建分组', + }, historyRange: '历史范围', import: '导入配置', importModal: { diff --git a/src/store/session/slices/session/action.ts b/src/store/session/slices/session/action.ts index 9b28c6753759..bd7af495adf6 100644 --- a/src/store/session/slices/session/action.ts +++ b/src/store/session/slices/session/action.ts @@ -61,6 +61,7 @@ export interface SessionAction { * switch session url */ switchSession: (sessionId?: string) => void; + updateSessionGroup: (sessionId: string, group: string) => Promise; /** * A custom hook that uses SWR to fetch sessions data. */ @@ -153,6 +154,12 @@ export const createSessionSlice: StateCreator< router?.push(SESSION_CHAT_URL(sessionId, isMobile)); }, + updateSessionGroup: async (sessionId, group) => { + await sessionService.updateSessionGroup(sessionId, group); + + await get().refreshSessions(); + }, + useFetchSessions: () => useSWR(FETCH_SESSIONS_KEY, sessionService.getSessions, { onSuccess: (data) => { diff --git a/src/store/session/slices/session/selectors/list.ts b/src/store/session/slices/session/selectors/list.ts index 56bc6d2dcff4..34ab1e8a9af3 100644 --- a/src/store/session/slices/session/selectors/list.ts +++ b/src/store/session/slices/session/selectors/list.ts @@ -16,6 +16,22 @@ const pinnedSessionList = (s: SessionStore) => const unpinnedSessionList = (s: SessionStore) => defaultSessions(s).filter((s) => s.group === SessionGroupDefaultKeys.Default); +const customSessionGroup = (s: SessionStore) => { + const group2SessionList: Record = {}; + // 1. 筛选用户自定义的session + const customList = defaultSessions(s).filter( + (s) => + s.group !== SessionGroupDefaultKeys.Pinned && s.group !== SessionGroupDefaultKeys.Default, + ); + // 2. 进行分组 + for (const s of customList) { + if (!s.group) continue; + group2SessionList[s.group] = group2SessionList[s.group] || []; + group2SessionList[s.group].push(s); + } + return group2SessionList; +}; + const getSessionById = (id: string) => (s: SessionStore): LobeAgentSession => @@ -57,6 +73,7 @@ const isSomeSessionActive = (s: SessionStore) => !!s.activeId && isSessionListIn export const sessionSelectors = { currentSession, currentSessionSafe, + customSessionGroup, getSessionById, getSessionMetaById, hasCustomAgents, From 462295374148707451acd2fdc6c3de79058f6a71 Mon Sep 17 00:00:00 2001 From: canisminor1990 Date: Mon, 15 Jan 2024 18:42:30 +0800 Subject: [PATCH 02/20] =?UTF-8?q?=E2=9C=A8=20feat:=20Add=20session=20group?= =?UTF-8?q?=20manager?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/ChatInput/Footer/index.tsx | 23 +- .../(desktop)/features/ChatInput/index.tsx | 2 +- .../features/ChatHeader/ShareButton/index.tsx | 1 - src/app/chat/features/Migration/Failed.tsx | 2 + .../CollapseGroup/Actions.tsx | 99 +++++ .../CollapseGroup/index.tsx | 6 + .../SessionListContent/DefaultMode.tsx | 89 +++-- .../SessionListContent/List/Item/Actions.tsx | 106 +++--- .../List/Item/CreateGroupModal.tsx | 45 --- .../SessionListContent/List/Item/index.tsx | 68 ++-- .../Modals/ConfigGroupModal/GroupItem.tsx | 88 +++++ .../Modals/ConfigGroupModal/index.tsx | 77 ++++ .../Modals/CreateGroupModal.tsx | 59 +++ .../Modals/RenameGroupModal.tsx | 54 +++ src/const/settings.ts | 1 + src/database/models/__tests__/session.test.ts | 4 +- src/database/models/session.ts | 4 +- src/features/ChatInput/ActionBar/Clear.tsx | 3 +- .../Conversation/Actions/Assistant.tsx | 2 +- .../Conversation/Actions/Function.tsx | 4 +- src/features/Conversation/Actions/User.tsx | 2 +- .../hooks/useChatListActionsBar.tsx | 6 +- src/features/PluginDevModal/index.tsx | 3 + .../PluginStore/PluginItem/Action.tsx | 2 + src/locales/default/chat.ts | 13 +- src/locales/default/common.ts | 7 - src/services/session.ts | 4 +- src/store/global/helpers.ts | 2 + src/store/global/slices/common/action.ts | 17 + .../global/slices/common/initialState.ts | 9 +- .../__snapshots__/selectors.test.ts.snap | 98 +++++ src/store/global/slices/settings/action.ts | 27 +- src/store/global/slices/settings/helpers.ts | 42 +++ .../global/slices/settings/selectors.test.ts | 339 ++++++++---------- src/store/global/slices/settings/selectors.ts | 3 + .../session/slices/session/action.test.ts | 54 +++ src/store/session/slices/session/action.ts | 8 +- src/store/session/slices/session/helpers.ts | 4 +- .../session/slices/session/initialState.ts | 34 +- .../session/slices/session/selectors/list.ts | 63 ++-- src/styles/antdOverride.ts | 7 - src/types/session.ts | 11 +- src/types/settings.ts | 2 + 43 files changed, 1037 insertions(+), 457 deletions(-) create mode 100644 src/app/chat/features/SessionListContent/CollapseGroup/Actions.tsx delete mode 100644 src/app/chat/features/SessionListContent/List/Item/CreateGroupModal.tsx create mode 100644 src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx create mode 100644 src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/index.tsx create mode 100644 src/app/chat/features/SessionListContent/Modals/CreateGroupModal.tsx create mode 100644 src/app/chat/features/SessionListContent/Modals/RenameGroupModal.tsx create mode 100644 src/store/global/slices/settings/helpers.ts diff --git a/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx b/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx index ae5b09505418..75417a8c39c3 100644 --- a/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx +++ b/src/app/chat/(desktop)/features/ChatInput/Footer/index.tsx @@ -4,12 +4,13 @@ import { createStyles } from 'antd-style'; import { ChevronUp, CornerDownLeft, - Loader2, LucideCheck, LucideChevronDown, LucideCommand, LucidePlus, + StopCircle, } from 'lucide-react'; +import { rgba } from 'polished'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Center, Flexbox } from 'react-layout-kit'; @@ -25,7 +26,7 @@ import { isMacOS } from '@/utils/platform'; import { LocalFiles } from './LocalFiles'; -const useStyles = createStyles(({ css, prefixCls }) => { +const useStyles = createStyles(({ css, prefixCls, token }) => { return { arrow: css` &.${prefixCls}-btn.${prefixCls}-btn-icon-only { @@ -38,13 +39,19 @@ const useStyles = createStyles(({ css, prefixCls }) => { align-items: center; justify-content: center; } + + .${prefixCls}-btn.${prefixCls}-dropdown-trigger { + &::before { + background-color: ${rgba(token.colorBgLayout, 0.1)} !important; + } + } `, }; }); const isMac = isMacOS(); -const Footer = memo(() => { +const Footer = memo<{ setExpand?: (expand: boolean) => void }>(({ setExpand }) => { const { t } = useTranslation('chat'); const { theme, styles } = useStyles(); @@ -104,12 +111,18 @@ const Footer = memo(() => { {loading ? ( - ) : ( - { >