diff --git a/src/app/chat/features/SessionListContent/CollapseGroup/Actions.tsx b/src/app/chat/features/SessionListContent/CollapseGroup/Actions.tsx index 40e696732ec12..229371383f341 100644 --- a/src/app/chat/features/SessionListContent/CollapseGroup/Actions.tsx +++ b/src/app/chat/features/SessionListContent/CollapseGroup/Actions.tsx @@ -1,14 +1,11 @@ import { ActionIcon, Icon } from '@lobehub/ui'; import { App, Dropdown, DropdownProps, MenuProps } from 'antd'; import { createStyles } from 'antd-style'; -import isEqual from 'fast-deep-equal'; import { MoreVertical, PencilLine, Settings2, Trash } from 'lucide-react'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { useGlobalStore } from '@/store/global'; -import { groupHelpers } from '@/store/global/helpers'; -import { settingsSelectors } from '@/store/global/selectors'; +import { useSessionStore } from '@/store/session'; const useStyles = createStyles(({ css }) => ({ modalRoot: css` @@ -25,8 +22,8 @@ const Actions = memo(({ id, openRenameModal, openConfigModal, onOp const { t } = useTranslation('chat'); const { styles } = useStyles(); const { modal } = App.useApp(); - const sessionCustomGroups = useGlobalStore(settingsSelectors.sessionCustomGroups, isEqual); - const updateCustomGroup = useGlobalStore((s) => s.updateCustomGroup); + + const [removeSessionGroup] = useSessionStore((s) => [s.removeSessionGroup]); const items: MenuProps['items'] = useMemo( () => [ { @@ -61,7 +58,7 @@ const Actions = memo(({ id, openRenameModal, openConfigModal, onOp centered: true, okButtonProps: { danger: true }, onOk: () => { - updateCustomGroup(groupHelpers.removeGroup(id, sessionCustomGroups)); + removeSessionGroup(id); }, rootClassName: styles.modalRoot, title: t('sessionGroup.confirmRemoveGroupAlert'), diff --git a/src/app/chat/features/SessionListContent/DefaultMode.tsx b/src/app/chat/features/SessionListContent/DefaultMode.tsx index c06f6f6d3b86f..797a222195992 100644 --- a/src/app/chat/features/SessionListContent/DefaultMode.tsx +++ b/src/app/chat/features/SessionListContent/DefaultMode.tsx @@ -4,9 +4,10 @@ import { memo, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useGlobalStore } from '@/store/global'; -import { preferenceSelectors, settingsSelectors } from '@/store/global/selectors'; +import { preferenceSelectors } from '@/store/global/selectors'; import { useSessionStore } from '@/store/session'; import { sessionSelectors } from '@/store/session/selectors'; +import { SessionDefaultGroup } from '@/types/session'; import Actions from '../SessionListContent/CollapseGroup/Actions'; import CollapseGroup from './CollapseGroup'; @@ -16,35 +17,34 @@ import ConfigGroupModal from './Modals/ConfigGroupModal'; import RenameGroupModal from './Modals/RenameGroupModal'; const SessionListContent = memo(() => { + const { t } = useTranslation('chat'); + const [activeGroupId, setActiveGroupId] = useState(); const [renameGroupModalOpen, setRenameGroupModalOpen] = useState(false); const [configGroupModalOpen, setConfigGroupModalOpen] = useState(false); - const { t } = useTranslation('chat'); - const sessionCustomGroups = useGlobalStore(settingsSelectors.sessionCustomGroups, isEqual); - const sessionList = - useSessionStore(sessionSelectors.sessionList(sessionCustomGroups), isEqual) || {}; + + const [useFetchSessions] = useSessionStore((s) => [s.useFetchSessions]); + useFetchSessions(); + + const pinnedSessions = useSessionStore(sessionSelectors.pinnedSessions, isEqual); + const defaultSessions = useSessionStore(sessionSelectors.defaultSessions, isEqual); + const customSessionGroups = useSessionStore(sessionSelectors.customSessionGroups, isEqual); + const [sessionGroupKeys, updatePreference] = useGlobalStore((s) => [ preferenceSelectors.sessionGroupKeys(s), s.updatePreference, ]); - const [ useFetchSessions] = useSessionStore((s) => [ - s.useFetchSessions, - ]); - useFetchSessions(); - const items = useMemo( () => [ - sessionList.pinnedList?.length > 0 && { - children: , - key: 'pinned', + pinnedSessions.length > 0 && { + children: , + key: SessionDefaultGroup.Pinned, label: t('pin'), }, - ...sessionCustomGroups.map(({ id, name }) => ({ - children: sessionList.customList[id] && ( - - ), + ...customSessionGroups.map(({ id, name, children }) => ({ + children: , extra: ( { label: name, })), { - children: , - key: 'defaultList', + children: , + key: SessionDefaultGroup.Default, label: t('defaultList'), }, ].filter(Boolean) as CollapseProps['items'], - [sessionCustomGroups, sessionList], + [customSessionGroups, pinnedSessions, defaultSessions], ); return ( @@ -74,8 +74,9 @@ const SessionListContent = memo(() => { activeKey={sessionGroupKeys} items={items} onChange={(keys) => { - const sessionGroupKeys = typeof keys === 'string' ? [keys] : keys; - updatePreference({ sessionGroupKeys }); + const expandSessionGroupKeys = typeof keys === 'string' ? [keys] : keys; + + updatePreference({ expandSessionGroupKeys }); }} /> {activeGroupId && ( diff --git a/src/app/chat/features/SessionListContent/List/Item/Actions.tsx b/src/app/chat/features/SessionListContent/List/Item/Actions.tsx index 945807c73a55f..2fbf7b26f8f87 100644 --- a/src/app/chat/features/SessionListContent/List/Item/Actions.tsx +++ b/src/app/chat/features/SessionListContent/List/Item/Actions.tsx @@ -17,11 +17,9 @@ import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { configService } from '@/services/config'; -import { useGlobalStore } from '@/store/global'; -import { settingsSelectors } from '@/store/global/selectors'; import { useSessionStore } from '@/store/session'; import { sessionHelpers } from '@/store/session/helpers'; -import { sessionSelectors } from '@/store/session/selectors'; +import { sessionGroupSelectors, sessionSelectors } from '@/store/session/selectors'; import { SessionDefaultGroup } from '@/types/session'; const useStyles = createStyles(({ css }) => ({ @@ -38,11 +36,10 @@ interface ActionProps { } const Actions = memo(({ group, id, openCreateGroupModal, setOpen }) => { - const { t } = useTranslation('chat'); - const { styles } = useStyles(); + const { t } = useTranslation('chat'); - const sessionCustomGroups = useGlobalStore(settingsSelectors.sessionCustomGroups, isEqual); + const sessionCustomGroups = useSessionStore(sessionGroupSelectors.sessionGroupItems, isEqual); const [pin, removeSession, pinSession, duplicateSession, updateSessionGroup] = useSessionStore( (s) => { const session = sessionSelectors.getSessionById(id)(s); @@ -51,7 +48,7 @@ const Actions = memo(({ group, id, openCreateGroupModal, setOpen }) s.removeSession, s.pinSession, s.duplicateSession, - s.updateSessionGroup, + s.updateSessionGroupId, ]; }, ); diff --git a/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx b/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx index 570a530613256..488b969b66102 100644 --- a/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx +++ b/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/GroupItem.tsx @@ -1,14 +1,11 @@ import { ActionIcon, EditableText, SortableList } from '@lobehub/ui'; -import { Popconfirm, message } from 'antd'; +import { App, Popconfirm } from 'antd'; import { createStyles } from 'antd-style'; -import isEqual from 'fast-deep-equal'; import { PencilLine, Trash } from 'lucide-react'; -import { memo, useCallback, useState } from 'react'; +import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useGlobalStore } from '@/store/global'; -import { groupHelpers } from '@/store/global/helpers'; -import { settingsSelectors } from '@/store/global/selectors'; +import { useSessionStore } from '@/store/session'; import { SessionGroupItem } from '@/types/session'; const useStyles = createStyles(({ css }) => ({ @@ -26,22 +23,15 @@ const useStyles = createStyles(({ css }) => ({ })); const GroupItem = memo(({ id, name }) => { - const [editing, setEditing] = useState(false); - const sessionCustomGroups = useGlobalStore(settingsSelectors.sessionCustomGroups, isEqual); - const updateCustomGroup = useGlobalStore((s) => s.updateCustomGroup); const { t } = useTranslation('chat'); const { styles } = useStyles(); + const { message } = App.useApp(); - const handleRename = useCallback( - (input: string) => { - if (!input) return; - if (input.length === 0 || input.length > 20) - return message.warning(t('sessionGroup.tooLong')); - updateCustomGroup(groupHelpers.renameGroup(id, input, sessionCustomGroups)); - message.success(t('sessionGroup.renameSuccess')); - }, - [id, sessionCustomGroups], - ); + const [editing, setEditing] = useState(false); + const [updateSessionGroupName, removeSessionGroup] = useSessionStore((s) => [ + s.updateSessionGroupName, + s.removeSessionGroup, + ]); return ( <> @@ -57,7 +47,7 @@ const GroupItem = memo(({ id, name }) => { type: 'primary', }} onConfirm={() => { - updateCustomGroup(groupHelpers.removeGroup(id, sessionCustomGroups)); + removeSessionGroup(id); }} title={t('sessionGroup.confirmRemoveGroupAlert')} > @@ -67,8 +57,14 @@ const GroupItem = memo(({ id, name }) => { ) : ( { - if (name !== v) handleRename(v); + onChangeEnd={(input) => { + if (name !== input) { + if (!input) return; + if (input.length === 0 || input.length > 20) + return message.warning(t('sessionGroup.tooLong')); + updateSessionGroupName(id, input); + message.success(t('sessionGroup.renameSuccess')); + } setEditing(false); }} onEditingChange={(e) => setEditing(e)} diff --git a/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/index.tsx b/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/index.tsx index 156f6681302d5..b3bfdee0dad6c 100644 --- a/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/index.tsx +++ b/src/app/chat/features/SessionListContent/Modals/ConfigGroupModal/index.tsx @@ -7,8 +7,8 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Flexbox } from 'react-layout-kit'; -import { useGlobalStore } from '@/store/global'; -import { settingsSelectors } from '@/store/global/selectors'; +import { useSessionStore } from '@/store/session'; +import { sessionGroupSelectors } from '@/store/session/selectors'; import { SessionGroupItem } from '@/types/session'; import GroupItem from './GroupItem'; @@ -30,10 +30,10 @@ const useStyles = createStyles(({ css, token, stylish }) => ({ const ConfigGroupModal = memo(({ open, onCancel }) => { const { t } = useTranslation('chat'); const { styles } = useStyles(); - const sessionCustomGroups = useGlobalStore(settingsSelectors.sessionCustomGroups, isEqual); - const [addCustomGroup, updateCustomGroup] = useGlobalStore((s) => [ - s.addCustomGroup, - s.updateCustomGroup, + const sessionGroupItems = useSessionStore(sessionGroupSelectors.sessionGroupItems, isEqual); + const [addSessionGroup, updateSessionGroupSort] = useSessionStore((s) => [ + s.addSessionGroup, + s.updateSessionGroupSort, ]); return ( @@ -47,8 +47,10 @@ const ConfigGroupModal = memo(({ open, onCancel }) => { > updateCustomGroup(item)} + items={sessionGroupItems} + onChange={(items: SessionGroupItem[]) => { + updateSessionGroupSort(items); + }} renderItem={(item: SessionGroupItem) => ( (({ open, onCancel }) => { diff --git a/src/app/chat/features/SessionListContent/Modals/CreateGroupModal.tsx b/src/app/chat/features/SessionListContent/Modals/CreateGroupModal.tsx index 36965df9a3bc1..c1445e4de1029 100644 --- a/src/app/chat/features/SessionListContent/Modals/CreateGroupModal.tsx +++ b/src/app/chat/features/SessionListContent/Modals/CreateGroupModal.tsx @@ -1,11 +1,10 @@ import { Input, Modal, type ModalProps } from '@lobehub/ui'; -import { message } from 'antd'; -import isEqual from 'fast-deep-equal'; -import { memo, useCallback, useState } from 'react'; +import { App } from 'antd'; +import { MouseEvent, memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { Flexbox } from 'react-layout-kit'; import { useGlobalStore } from '@/store/global'; -import { settingsSelectors } from '@/store/global/selectors'; import { useSessionStore } from '@/store/session'; interface CreateGroupModalProps extends ModalProps { @@ -15,41 +14,46 @@ interface CreateGroupModalProps extends ModalProps { const CreateGroupModal = memo( ({ id, open, onCancel }: CreateGroupModalProps) => { const { t } = useTranslation('chat'); - const sessionCustomGroups = useGlobalStore(settingsSelectors.sessionCustomGroups, isEqual); - const addCustomGroup = useGlobalStore((s) => s.addCustomGroup); - const updateSessionGroup = useSessionStore((s) => s.updateSessionGroup); - const [input, setInput] = useState(''); - const handleSubmit = useCallback( - (e: any) => { - if (!input) return; - if (input.length === 0 || input.length > 20) - return message.warning(t('sessionGroup.tooLong')); - - const groupId = addCustomGroup(input); - updateSessionGroup(id, groupId); - message.success(t('sessionGroup.createSuccess')); - onCancel?.(e); - }, - [id, input, sessionCustomGroups], - ); + const toggleExpandSessionGroup = useGlobalStore((s) => s.toggleExpandSessionGroup); + const { message } = App.useApp(); + const [updateSessionGroup, addCustomGroup] = useSessionStore((s) => [ + s.updateSessionGroupId, + s.addSessionGroup, + ]); + const [input, setInput] = useState(''); return (
e.stopPropagation()}> ) => { + if (!input) return; + + if (input.length === 0 || input.length > 20) + return message.warning(t('sessionGroup.tooLong')); + + const groupId = await addCustomGroup(input); + await updateSessionGroup(id, groupId); + toggleExpandSessionGroup(groupId, true); + + message.success(t('sessionGroup.createSuccess')); + onCancel?.(e); + }} open={open} title={t('sessionGroup.createGroup')} width={400} > - setInput(e.target.value)} - placeholder={t('sessionGroup.inputPlaceholder')} - value={input} - /> + + setInput(e.target.value)} + placeholder={t('sessionGroup.inputPlaceholder')} + value={input} + /> +
); diff --git a/src/app/chat/features/SessionListContent/Modals/RenameGroupModal.tsx b/src/app/chat/features/SessionListContent/Modals/RenameGroupModal.tsx index aa4d316629b97..d41f9a3468dee 100644 --- a/src/app/chat/features/SessionListContent/Modals/RenameGroupModal.tsx +++ b/src/app/chat/features/SessionListContent/Modals/RenameGroupModal.tsx @@ -1,12 +1,11 @@ import { Input, Modal, type ModalProps } from '@lobehub/ui'; -import { message } from 'antd'; +import { App } from 'antd'; import isEqual from 'fast-deep-equal'; -import { memo, useCallback, useState } from 'react'; +import { memo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useGlobalStore } from '@/store/global'; -import { groupHelpers } from '@/store/global/helpers'; -import { settingsSelectors } from '@/store/global/selectors'; +import { useSessionStore } from '@/store/session'; +import { sessionGroupSelectors } from '@/store/session/selectors'; interface RenameGroupModalProps extends ModalProps { id: string; @@ -14,28 +13,25 @@ interface RenameGroupModalProps extends ModalProps { const RenameGroupModal = memo(({ id, open, onCancel }) => { const { t } = useTranslation('chat'); - const sessionCustomGroups = useGlobalStore(settingsSelectors.sessionCustomGroups, isEqual); - const updateCustomGroup = useGlobalStore((s) => s.updateCustomGroup); - const group = groupHelpers.getGroupById(id, sessionCustomGroups); - const [input, setInput] = useState(); - const handleSubmit = useCallback( - (e: any) => { - if (!input) return; - if (input.length === 0 || input.length > 20) - return message.warning(t('sessionGroup.tooLong')); - updateCustomGroup(groupHelpers.renameGroup(id, input, sessionCustomGroups)); - message.success(t('sessionGroup.renameSuccess')); - onCancel?.(e); - }, - [id, input, sessionCustomGroups], - ); + const updateSessionGroupName = useSessionStore((s) => s.updateSessionGroupName); + const group = useSessionStore((s) => sessionGroupSelectors.getGroupById(id)(s), isEqual); + + const [input, setInput] = useState(); + const { message } = App.useApp(); return ( { + if (!input) return; + if (input.length === 0 || input.length > 20) + return message.warning(t('sessionGroup.tooLong')); + updateSessionGroupName(id, input); + message.success(t('sessionGroup.renameSuccess')); + onCancel?.(e); + }} open={open} title={t('sessionGroup.rename')} width={400} diff --git a/src/const/settings.ts b/src/const/settings.ts index d8e691eb2cd17..82fdde3b62ffb 100644 --- a/src/const/settings.ts +++ b/src/const/settings.ts @@ -15,7 +15,6 @@ export const DEFAULT_BASE_SETTINGS: GlobalBaseSettings = { fontSize: 14, language: 'auto', password: '', - sessionCustomGroups: [], themeMode: 'auto', }; diff --git a/src/database/core/db.ts b/src/database/core/db.ts index b47add44ef2bf..59615bb821ffd 100644 --- a/src/database/core/db.ts +++ b/src/database/core/db.ts @@ -5,14 +5,16 @@ import { DB_File } from '@/database/schemas/files'; import { DB_Message } from '@/database/schemas/message'; import { DB_Plugin } from '@/database/schemas/plugin'; import { DB_Session } from '@/database/schemas/session'; +import { DB_SessionGroup } from '@/database/schemas/sessionGroup'; import { DB_Topic } from '@/database/schemas/topic'; -import { dbSchemaV1, dbSchemaV2, dbSchemaV3 } from './schemas'; +import { dbSchemaV1, dbSchemaV2, dbSchemaV3, dbSchemaV4 } from './schemas'; interface LobeDBSchemaMap { files: DB_File; messages: DB_Message; plugins: DB_Plugin; + sessionGroups: DB_SessionGroup; sessions: DB_Session; topics: DB_Topic; } @@ -24,18 +26,21 @@ export class LocalDB extends Dexie { public messages: LobeDBTable<'messages'>; public topics: LobeDBTable<'topics'>; public plugins: LobeDBTable<'plugins'>; + public sessionGroups: LobeDBTable<'sessionGroups'>; constructor() { super('LOBE_CHAT_DB'); this.version(1).stores(dbSchemaV1); this.version(2).stores(dbSchemaV2); this.version(3).stores(dbSchemaV3); + this.version(4).stores(dbSchemaV4); this.files = this.table('files'); this.sessions = this.table('sessions'); this.messages = this.table('messages'); this.topics = this.table('topics'); this.plugins = this.table('plugins'); + this.sessionGroups = this.table('sessionGroups'); } } diff --git a/src/database/core/schemas.ts b/src/database/core/schemas.ts index a7449eea63576..cfe7adf22c15e 100644 --- a/src/database/core/schemas.ts +++ b/src/database/core/schemas.ts @@ -24,10 +24,23 @@ export const dbSchemaV2 = { // ************************************** // // ******* Version 3 - 2023-12-06 ******* // // ************************************** // -// - Added `plugin` table +// - Added `plugins` table export const dbSchemaV3 = { ...dbSchemaV2, plugins: '&identifier, type, manifest.type, manifest.meta.title, manifest.meta.description, manifest.meta.author, createdAt, updatedAt', }; + +// ************************************** // +// ******* Version 4 - 2024-01-21 ******* // +// ************************************** // +// - Added `sessionGroups` table +// - Add `pinned` to sessions table + +export const dbSchemaV4 = { + ...dbSchemaV3, + sessionGroups: '&id, name, sort, createdAt, updatedAt', + sessions: + '&id, type, group, pinned, meta.title, meta.description, meta.tags, createdAt, updatedAt', +}; diff --git a/src/database/models/session.ts b/src/database/models/session.ts index f90f08baf9f6d..2d7cd33ffdd47 100644 --- a/src/database/models/session.ts +++ b/src/database/models/session.ts @@ -3,9 +3,16 @@ import { DeepPartial } from 'utility-types'; import { DEFAULT_AGENT_LOBE_SESSION } from '@/const/session'; import { BaseModel } from '@/database/core'; import { DBModel } from '@/database/core/types/db'; +import { SessionGroupModel } from '@/database/models/sessionGroup'; import { DB_Session, DB_SessionSchema } from '@/database/schemas/session'; import { LobeAgentConfig } from '@/types/agent'; -import { LobeAgentSession, LobeSessions, SessionGroupId } from '@/types/session'; +import { + ChatSessionList, + LobeAgentSession, + LobeSessions, + SessionDefaultGroup, + SessionGroupId, +} from '@/types/session'; import { merge } from '@/utils/merge'; import { uuid } from '@/utils/uuid'; @@ -36,10 +43,19 @@ class _SessionModel extends BaseModel { * get sessions by group * @param group */ - async queryByGroup(group: SessionGroupId) { + async queryByGroup(group: SessionGroupId): Promise { return this.table.where('group').equals(group).toArray(); } + async queryByGroupIds(groups: string[]) { + const pools = groups.map(async (id) => { + return [id, await this.queryByGroup(id)] as const; + }); + const groupItems = await Promise.all(pools); + + return Object.fromEntries(groupItems); + } + async update(id: string, data: Partial) { return super._update(id, data); } @@ -165,6 +181,26 @@ class _SessionModel extends BaseModel { return this._add(newSession, uuid()); } + + async queryWithGroups(): Promise { + const groups = await SessionGroupModel.query(); + const customGroups = await this.queryByGroupIds(groups.map((item) => item.id)); + + const defaultItems = await this.queryByGroup(SessionDefaultGroup.Default); + const pinnedItems = await this.queryByGroup(SessionDefaultGroup.Pinned); + // const pinnedItems = await this.table.where('pinned').equals(1).toArray(); + + const all = await this.query(); + return { + all, + customGroup: groups.map((group) => ({ + ...group, + children: customGroups[group.id], + })), + default: defaultItems, + pinned: pinnedItems, + }; + } } export const SessionModel = new _SessionModel(); diff --git a/src/database/models/sessionGroup.ts b/src/database/models/sessionGroup.ts new file mode 100644 index 0000000000000..51d750d18df81 --- /dev/null +++ b/src/database/models/sessionGroup.ts @@ -0,0 +1,63 @@ +import { BaseModel } from '@/database/core'; +import { DB_SessionGroup, DB_SessionGroupSchema } from '@/database/schemas/sessionGroup'; +import { SessionGroups } from '@/types/session'; +import { nanoid } from '@/utils/uuid'; + +class _SessionGroupModel extends BaseModel { + constructor() { + super('sessionGroups', DB_SessionGroupSchema); + } + + async create(name: string, sort?: number, id = nanoid()) { + return this._add({ name, sort }, id); + } + + async batchCreate(groups: SessionGroups) { + return this._batchAdd(groups, { idGenerator: nanoid }); + } + + async update(id: string, data: Partial) { + return super._update(id, data); + } + + async delete(id: string, removeGroupItem: boolean = false) { + if (!removeGroupItem) return this.table.delete(id); + + // TODO: delete all session associated with the sessionGroup + } + + async query(): Promise { + const allGroups = await this.table.toArray(); + + // 自定义排序,先按 sort 存在与否分组,然后分别排序 + return allGroups.sort((a, b) => { + // 如果两个项都有 sort,则按 sort 排序 + if (a.sort !== undefined && b.sort !== undefined) { + return a.sort - b.sort; + } + // 如果 a 有 sort 而 b 没有,则 a 排在前面 + if (a.sort !== undefined) { + return -1; + } + // 如果 b 有 sort 而 a 没有,则 b 排在前面 + if (b.sort !== undefined) { + return 1; + } + // 如果两个项都没有 sort,则按 createdAt 倒序排序 + if (a.createdAt && b.createdAt) { + return b.createdAt - a.createdAt; + } + return 0; + }); + } + + async updateOrder(sortMap: { id: string; sort: number }[]) { + return this.db.transaction('rw', this.table, async () => { + for (const { id, sort } of sortMap) { + await this.table.update(id, { sort }); + } + }); + } +} + +export const SessionGroupModel = new _SessionGroupModel(); diff --git a/src/database/schemas/sessionGroup.ts b/src/database/schemas/sessionGroup.ts new file mode 100644 index 0000000000000..4bef20d01585c --- /dev/null +++ b/src/database/schemas/sessionGroup.ts @@ -0,0 +1,8 @@ +import { z } from 'zod'; + +export const DB_SessionGroupSchema = z.object({ + name: z.string(), + sort: z.number().optional(), +}); + +export type DB_SessionGroup = z.infer; diff --git a/src/services/__tests__/session.test.ts b/src/services/__tests__/session.test.ts index 390c6dbacbcb0..32fcf5695c977 100644 --- a/src/services/__tests__/session.test.ts +++ b/src/services/__tests__/session.test.ts @@ -21,6 +21,7 @@ vi.mock('@/database/models/session', () => { isEmpty: vi.fn(), queryByKeyword: vi.fn(), updateConfig: vi.fn(), + queryByGroupIds: vi.fn(), }, }; }); @@ -140,14 +141,14 @@ describe('SessionService', () => { }); }); - describe('updateSessionGroup', () => { + describe('updateSessionGroupId', () => { it('should update the group of a session', async () => { // Setup const groupId = 'new-group'; (SessionModel.update as Mock).mockResolvedValue({ ...mockSession, group: groupId }); // Execute - const result = await sessionService.updateSessionGroup(mockSessionId, groupId); + const result = await sessionService.updateSessionGroupId(mockSessionId, groupId); // Assert expect(SessionModel.update).toHaveBeenCalledWith(mockSessionId, { group: groupId }); diff --git a/src/services/config.ts b/src/services/config.ts index 61a8113d9987b..14e8cc3e5048e 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -4,7 +4,7 @@ import { topicService } from '@/services/topic'; import { useGlobalStore } from '@/store/global'; import { settingsSelectors } from '@/store/global/selectors'; import { useSessionStore } from '@/store/session'; -import { sessionExportSelectors, sessionSelectors } from '@/store/session/selectors'; +import { sessionSelectors } from '@/store/session/selectors'; import { ConfigFile } from '@/types/exportConfig'; import { ChatMessage } from '@/types/message'; import { LobeSessions } from '@/types/session'; @@ -189,7 +189,7 @@ class ConfigService { sessionSelectors.getSessionById(id)(useSessionStore.getState()); private getAgent = (id: string) => - sessionExportSelectors.getExportAgent(id)(useSessionStore.getState()); + sessionSelectors.getSessionById(id)(useSessionStore.getState()); private mapImportResult = (input: { added: number; diff --git a/src/services/session.ts b/src/services/session.ts index ae0f4a9c2b92b..15002ae3020bb 100644 --- a/src/services/session.ts +++ b/src/services/session.ts @@ -1,9 +1,18 @@ import { DeepPartial } from 'utility-types'; import { SessionModel } from '@/database/models/session'; +import { SessionGroupModel } from '@/database/models/sessionGroup'; import { LobeAgentConfig } from '@/types/agent'; import { MetaData } from '@/types/meta'; -import { LobeAgentSession, LobeSessionType, LobeSessions, SessionGroupId } from '@/types/session'; +import { + ChatSessionList, + LobeAgentSession, + LobeSessionType, + LobeSessions, + SessionGroupId, + SessionGroupItem, + SessionGroups, +} from '@/types/session'; class SessionService { async createNewSession( @@ -21,24 +30,39 @@ class SessionService { return SessionModel.batchCreate(importSessions); } + async duplicateSession(id: string, newTitle: string): Promise { + const res = await SessionModel.duplicate(id, newTitle); + + if (res) return res?.id; + } + async getSessions(): Promise { + const groups = await SessionGroupModel.query(); + const data = await SessionModel.queryByGroupIds(groups.map((item) => item.id)); + + console.log(data); return SessionModel.query(); } + async getSessionsWithGroup(): Promise { + return SessionModel.queryWithGroups(); + } + async getAllAgents(): Promise { // TODO: add a filter to get only agents return await SessionModel.query(); } - async removeSession(id: string) { - return SessionModel.delete(id); + async hasSessions() { + const isEmpty = await SessionModel.isEmpty(); + return !isEmpty; } - async removeAllSessions() { - return SessionModel.clearTable(); + async searchSessions(keyword: string) { + return SessionModel.queryByKeyword(keyword); } - async updateSessionGroup(id: string, group: SessionGroupId) { + async updateSessionGroupId(id: string, group: SessionGroupId) { return SessionModel.update(id, { group }); } @@ -50,19 +74,41 @@ class SessionService { return SessionModel.updateConfig(activeId, config); } - async hasSessions() { - const isEmpty = await SessionModel.isEmpty(); - return !isEmpty; + async removeSession(id: string) { + return SessionModel.delete(id); } - async searchSessions(keyword: string) { - return SessionModel.queryByKeyword(keyword); + async removeAllSessions() { + return SessionModel.clearTable(); } - async duplicateSession(id: string, newTitle: string): Promise { - const res = await SessionModel.duplicate(id, newTitle); + // ************************************** // + // *********** SessionGroup *********** // + // ************************************** // - if (res) return res?.id; + async createSessionGroup(name: string, sort?: number) { + const item = await SessionGroupModel.create(name, sort); + if (!item) { + throw new Error('session group create Error'); + } + + return item.id; + } + + async batchCreateSessionGroups(groups: SessionGroups) { + return SessionGroupModel.batchCreate(groups); + } + + async removeSessionGroup(id: string, removeChildren?: boolean) { + return SessionGroupModel.delete(id, removeChildren); + } + + async updateSessionGroup(id: string, data: Partial) { + return SessionGroupModel.update(id, data); + } + + async updateSessionGroupOrder(sortMap: { id: string; sort: number }[]) { + return SessionGroupModel.updateOrder(sortMap); } } diff --git a/src/store/global/slices/common/action.ts b/src/store/global/slices/common/action.ts index 7341eb8c1ee5d..8abfc5c5f67e1 100644 --- a/src/store/global/slices/common/action.ts +++ b/src/store/global/slices/common/action.ts @@ -11,7 +11,6 @@ import type { GlobalStore } from '@/store/global'; import type { GlobalServerConfig } from '@/types/settings'; import { merge } from '@/utils/merge'; import { setNamespace } from '@/utils/storeDebug'; -import { nanoid } from '@/utils/uuid'; import type { GlobalCommonState, GlobalPreference, Guide, SidebarTabKey } from './initialState'; @@ -21,7 +20,6 @@ const n = setNamespace('settings'); * 设置操作 */ export interface CommonAction { - addCustomGroup: (name: string) => string; switchBackToChat: (sessionId?: string) => void; /** * 切换侧边栏选项 @@ -29,6 +27,7 @@ export interface CommonAction { */ switchSideBar: (key: SidebarTabKey) => void; toggleChatSideBar: (visible?: boolean) => void; + toggleExpandSessionGroup: (id: string, expand: boolean) => void; toggleMobileTopic: (visible?: boolean) => void; toggleSystemRole: (visible?: boolean) => void; updateGuideState: (guide: Partial) => void; @@ -43,21 +42,6 @@ export const createCommonSlice: StateCreator< [], CommonAction > = (set, get) => ({ - addCustomGroup: (name) => { - const sessionCustomGroups = get().preference.sessionCustomGroups || []; - - const groupId = nanoid(); - const sessionGroupKeys = get().preference.sessionGroupKeys || []; - get().updatePreference( - { - sessionCustomGroups: [...sessionCustomGroups, { id: groupId, name }], - sessionGroupKeys: [...sessionGroupKeys, groupId], - }, - 'addCustomGroup', - ); - - return groupId; - }, switchBackToChat: (sessionId) => { get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile)); }, @@ -70,6 +54,19 @@ export const createCommonSlice: StateCreator< get().updatePreference({ showChatSideBar }, n('toggleAgentPanel', newValue) as string); }, + toggleExpandSessionGroup: (id, expand) => { + const { preference } = get(); + const nextExpandSessionGroup = produce(preference.expandSessionGroupKeys, (draft) => { + if (expand) { + if (draft.includes(id)) return; + draft.push(id); + } else { + const index = draft.indexOf(id); + if (index !== -1) draft.splice(index, 1); + } + }); + get().updatePreference({ expandSessionGroupKeys: nextExpandSessionGroup }); + }, toggleMobileTopic: (newValue) => { const mobileShowTopic = typeof newValue === 'boolean' ? newValue : !get().preference.mobileShowTopic; diff --git a/src/store/global/slices/common/initialState.ts b/src/store/global/slices/common/initialState.ts index 786b481dffb9f..cc10c8b2964ba 100644 --- a/src/store/global/slices/common/initialState.ts +++ b/src/store/global/slices/common/initialState.ts @@ -21,11 +21,12 @@ export interface Guide { } export interface GlobalPreference { + // which sessionGroup should expand + expandSessionGroupKeys: SessionGroupId[]; guide?: Guide; inputHeight: number; mobileShowTopic?: boolean; sessionCustomGroups: SessionGroupItem[]; - sessionGroupKeys: SessionGroupId[]; sessionsWidth: number; showChatSideBar?: boolean; @@ -54,11 +55,11 @@ export interface GlobalCommonState { export const initialCommonState: GlobalCommonState = { isMobile: false, preference: { + expandSessionGroupKeys: [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default], guide: {}, inputHeight: 200, mobileShowTopic: false, sessionCustomGroups: [], - sessionGroupKeys: [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default], sessionsWidth: 320, showChatSideBar: true, showSessionPanel: true, diff --git a/src/store/global/slices/common/selectors.ts b/src/store/global/slices/common/selectors.ts index 73df620157b9d..ab201e9b5ce6f 100644 --- a/src/store/global/slices/common/selectors.ts +++ b/src/store/global/slices/common/selectors.ts @@ -1,9 +1,6 @@ import { GlobalStore } from '@/store/global'; -import { initialState } from '../../initialState'; - -const sessionGroupKeys = (s: GlobalStore): string[] => - s.preference.sessionGroupKeys || initialState.preference.sessionGroupKeys; +const sessionGroupKeys = (s: GlobalStore): string[] => s.preference.expandSessionGroupKeys || []; const useCmdEnterToSend = (s: GlobalStore): boolean => s.preference.useCmdEnterToSend || false; diff --git a/src/store/global/slices/settings/action.test.ts b/src/store/global/slices/settings/action.test.ts index bbcc8baee0e92..9c9a8c1718f8c 100644 --- a/src/store/global/slices/settings/action.test.ts +++ b/src/store/global/slices/settings/action.test.ts @@ -5,8 +5,7 @@ import { describe, expect, it, vi } from 'vitest'; import { DEFAULT_AGENT, DEFAULT_SETTINGS } from '@/const/settings'; import { useGlobalStore } from '@/store/global'; import { SettingsTabs } from '@/store/global/initialState'; -import { LanguageModel } from '@/types/llm'; -import { LobeAgentSettings, SessionGroupItem } from '@/types/session'; +import { LobeAgentSettings } from '@/types/session'; import { GlobalSettings, OpenAIConfig } from '@/types/settings'; beforeEach(() => { @@ -18,23 +17,6 @@ vi.mock('@/utils/uuid', () => ({ })); describe('SettingsAction', () => { - describe('addCustomGroup', () => { - it('should add a custom group and update session group keys', () => { - const { result } = renderHook(() => useGlobalStore()); - - act(() => { - const groupId = result.current.addCustomGroup('New Group'); - expect(groupId).toBe('unique-id'); - }); - - expect(result.current.settings.sessionCustomGroups).toContainEqual({ - id: 'unique-id', - name: 'New Group', - }); - expect(result.current.preference.sessionGroupKeys).toContain('unique-id'); - }); - }); - describe('importAppSettings', () => { it('should import app settings', () => { const { result } = renderHook(() => useGlobalStore()); @@ -115,19 +97,6 @@ describe('SettingsAction', () => { }); }); - describe('updateCustomGroup', () => { - it('should update custom groups', () => { - const { result } = renderHook(() => useGlobalStore()); - const updatedGroups: SessionGroupItem[] = [{ id: 'group-id', name: 'Updated Group' }]; - - act(() => { - result.current.updateCustomGroup(updatedGroups); - }); - - expect(result.current.settings.sessionCustomGroups).toEqual(updatedGroups); - }); - }); - describe('updateDefaultAgent', () => { it('should update default agent settings', () => { const { result } = renderHook(() => useGlobalStore()); diff --git a/src/store/global/slices/settings/action.ts b/src/store/global/slices/settings/action.ts index 94eb152968674..ccd5b1dfccb6a 100644 --- a/src/store/global/slices/settings/action.ts +++ b/src/store/global/slices/settings/action.ts @@ -7,11 +7,10 @@ import type { StateCreator } from 'zustand/vanilla'; import { DEFAULT_AGENT, DEFAULT_SETTINGS } from '@/const/settings'; import type { GlobalStore } from '@/store/global'; import { SettingsTabs } from '@/store/global/initialState'; -import { LobeAgentSettings, SessionGroupItem } from '@/types/session'; +import { LobeAgentSettings } from '@/types/session'; import type { GlobalSettings, OpenAIConfig } from '@/types/settings'; import { merge } from '@/utils/merge'; import { setNamespace } from '@/utils/storeDebug'; -import { nanoid } from '@/utils/uuid'; const n = setNamespace('settings'); @@ -19,7 +18,6 @@ const n = setNamespace('settings'); * 设置操作 */ export interface SettingsAction { - addCustomGroup: (name: string) => string; importAppSettings: (settings: GlobalSettings) => void; /** * 重置设置 @@ -38,7 +36,6 @@ export interface SettingsAction { * @param themeMode - 主题模式 */ switchThemeMode: (themeMode: ThemeMode) => void; - updateCustomGroup: (groups: SessionGroupItem[]) => void; updateDefaultAgent: (agent: DeepPartial) => void; } @@ -48,21 +45,6 @@ export const createSettingsSlice: StateCreator< [], SettingsAction > = (set, get) => ({ - addCustomGroup: (name) => { - const sessionCustomGroups = get().settings.sessionCustomGroups || []; - - const groupId = nanoid(); - const sessionGroupKeys = get().preference.sessionGroupKeys || []; - get().updateCustomGroup([...sessionCustomGroups, { id: groupId, name }]); - get().updatePreference( - { - sessionGroupKeys: [...sessionGroupKeys, groupId], - }, - 'addCustomGroup', - ); - - return groupId; - }, importAppSettings: (importAppSettings) => { const { setSettings } = get(); // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -101,9 +83,6 @@ export const createSettingsSlice: StateCreator< switchThemeMode: (themeMode) => { get().setSettings({ themeMode }); }, - updateCustomGroup: (groups) => { - get().setSettings({ sessionCustomGroups: groups }); - }, updateDefaultAgent: (agent) => { const settings = produce(get().settings, (draft: GlobalSettings) => { draft.defaultAgent = merge(draft.defaultAgent, agent); diff --git a/src/store/global/slices/settings/selectors.test.ts b/src/store/global/slices/settings/selectors.test.ts index cfa861acdf636..a6d259d4881c6 100644 --- a/src/store/global/slices/settings/selectors.test.ts +++ b/src/store/global/slices/settings/selectors.test.ts @@ -292,23 +292,4 @@ describe('settingsSelectors', () => { expect(result).toBe(true); }); }); - - describe('sessionCustomGroups', () => { - it('should return session custom groups', () => { - const s = { - settings: { - sessionCustomGroups: [ - { - name: 'Group 1', - id: 'group-1', - }, - ], - }, - } as unknown as GlobalStore; - - const result = settingsSelectors.sessionCustomGroups(s); - - expect(result).toMatchSnapshot(); - }); - }); }); diff --git a/src/store/global/slices/settings/selectors.ts b/src/store/global/slices/settings/selectors.ts index 514e973cbd371..81112f8c1baed 100644 --- a/src/store/global/slices/settings/selectors.ts +++ b/src/store/global/slices/settings/selectors.ts @@ -89,8 +89,6 @@ const currentLanguage = (s: GlobalStore) => { const dalleConfig = (s: GlobalStore) => s.settings.tool?.dalle || {}; const isDalleAutoGenerating = (s: GlobalStore) => s.settings.tool?.dalle?.autoGenerate; -const sessionCustomGroups = (s: GlobalStore) => s.settings.sessionCustomGroups || []; - export const settingsSelectors = { currentLanguage, currentSettings, @@ -104,5 +102,4 @@ export const settingsSelectors = { modelList: modelListSelectors, openAIAPI: openAIAPIKeySelectors, openAIProxyUrl: openAIProxyUrlSelectors, - sessionCustomGroups, }; diff --git a/src/store/global/store.ts b/src/store/global/store.ts index 1e0e7f4d307db..a7287c8af4bba 100644 --- a/src/store/global/store.ts +++ b/src/store/global/store.ts @@ -5,6 +5,7 @@ import { createWithEqualityFn } from 'zustand/traditional'; import { StateCreator } from 'zustand/vanilla'; import { DEFAULT_AGENT, DEFAULT_LLM_CONFIG } from '@/const/settings'; +import { SessionDefaultGroup } from '@/types/session'; import { isDev } from '@/utils/env'; import { createHyperStorage } from '../middleware/createHyperStorage'; @@ -32,6 +33,12 @@ const persistOptions: PersistOptions = { return { ...currentState, ...state, + preference: produce(state.preference, (draft) => { + if (!draft.expandSessionGroupKeys) { + draft.expandSessionGroupKeys = [SessionDefaultGroup.Pinned, SessionDefaultGroup.Default]; + delete (draft as any).sessionGroupKeys; + } + }), settings: produce(state.settings, (draft) => { if (!draft.defaultAgent) { draft.defaultAgent = DEFAULT_AGENT; diff --git a/src/store/session/initialState.ts b/src/store/session/initialState.ts index db151d5024bc2..1db1d69d62f50 100644 --- a/src/store/session/initialState.ts +++ b/src/store/session/initialState.ts @@ -1,34 +1,9 @@ -import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; +import { SessionState, initialSessionState } from './slices/session/initialState'; +import { SessionGroupState, initSessionGroupState } from './slices/sessionGroup/initialState'; -import { LobeAgentSession } from '@/types/session'; - -export type SessionStoreState = SessionState; - -export interface SessionState { - /** - * @title 当前活动的会话 - * @description 当前正在编辑或查看的会话 - */ - activeId: string; - isMobile?: boolean; - isSearching: boolean; - isSessionsFirstFetchFinished: boolean; - /** - * 后续看看是否可以将 router 部分的逻辑移出去 - * @deprecated - */ - router?: AppRouterInstance; - searchKeywords: string; - searchSessions: LobeAgentSession[]; - sessions: LobeAgentSession[]; -} +export interface SessionStoreState extends SessionGroupState, SessionState {} export const initialState: SessionStoreState = { - activeId: 'inbox', - isMobile: false, - isSearching: false, - isSessionsFirstFetchFinished: false, - searchKeywords: '', - searchSessions: [], - sessions: [], + ...initSessionGroupState, + ...initialSessionState, }; diff --git a/src/store/session/selectors.ts b/src/store/session/selectors.ts index ba10b2cfd39d2..258f2805d386b 100644 --- a/src/store/session/selectors.ts +++ b/src/store/session/selectors.ts @@ -1,2 +1,3 @@ export { agentSelectors } from './slices/agent/selectors'; -export { sessionExportSelectors, sessionSelectors } from './slices/session/selectors'; +export { sessionSelectors } from './slices/session/selectors'; +export { sessionGroupSelectors } from './slices/sessionGroup/selectors'; diff --git a/src/store/session/slices/session/action.test.ts b/src/store/session/slices/session/action.test.ts index 7664449167369..3eefe577f8dda 100644 --- a/src/store/session/slices/session/action.test.ts +++ b/src/store/session/slices/session/action.test.ts @@ -17,6 +17,7 @@ vi.mock('@/services/session', () => ({ updateSessionGroup: vi.fn(), removeSession: vi.fn(), getSessions: vi.fn(), + updateSessionGroupId: vi.fn(), searchSessions: vi.fn(), }, })); @@ -156,7 +157,7 @@ describe('SessionAction', () => { await result.current.pinSession(sessionId, true); }); - expect(sessionService.updateSessionGroup).toHaveBeenCalledWith(sessionId, 'pinned'); + expect(sessionService.updateSessionGroupId).toHaveBeenCalledWith(sessionId, 'pinned'); expect(mockRefresh).toHaveBeenCalled(); }); @@ -168,22 +169,22 @@ describe('SessionAction', () => { await result.current.pinSession(sessionId, false); }); - expect(sessionService.updateSessionGroup).toHaveBeenCalledWith(sessionId, 'default'); + expect(sessionService.updateSessionGroupId).toHaveBeenCalledWith(sessionId, 'default'); expect(mockRefresh).toHaveBeenCalled(); }); }); - describe('updateSessionGroup', () => { + describe('updateSessionGroupId', () => { it('should update the session group and refresh the list', async () => { const { result } = renderHook(() => useSessionStore()); const sessionId = 'session-id'; const groupId = 'new-group-id'; await act(async () => { - await result.current.updateSessionGroup(sessionId, groupId); + await result.current.updateSessionGroupId(sessionId, groupId); }); - expect(sessionService.updateSessionGroup).toHaveBeenCalledWith(sessionId, groupId); + expect(sessionService.updateSessionGroupId).toHaveBeenCalledWith(sessionId, groupId); expect(mockRefresh).toHaveBeenCalled(); }); }); diff --git a/src/store/session/slices/session/action.ts b/src/store/session/slices/session/action.ts index ad53d636dc1ec..8771b57d1fb42 100644 --- a/src/store/session/slices/session/action.ts +++ b/src/store/session/slices/session/action.ts @@ -11,6 +11,7 @@ import { useGlobalStore } from '@/store/global'; import { settingsSelectors } from '@/store/global/selectors'; import { SessionStore } from '@/store/session'; import { + ChatSessionList, LobeAgentSession, LobeAgentSettings, LobeSessionType, @@ -61,7 +62,6 @@ export interface SessionAction { * switch session url */ switchSession: (sessionId?: string) => void; - updateSessionGroup: (sessionId: string, groupId: string) => void; /** * A custom hook that uses SWR to fetch sessions data. */ @@ -128,7 +128,7 @@ export const createSessionSlice: StateCreator< }, pinSession: async (sessionId, pinned) => { - await sessionService.updateSessionGroup(sessionId, pinned ? 'pinned' : 'default'); + await sessionService.updateSessionGroupId(sessionId, pinned ? 'pinned' : 'default'); await get().refreshSessions(); }, @@ -145,6 +145,7 @@ export const createSessionSlice: StateCreator< get().switchSession(); } }, + switchSession: (sessionId = INBOX_SESSION_ID) => { const { isMobile, router } = get(); @@ -153,26 +154,25 @@ export const createSessionSlice: StateCreator< // TODO: 后续可以把 router 移除 router?.push(SESSION_CHAT_URL(sessionId, isMobile)); }, - updateSessionGroup: async (sessionId, groupId) => { - await sessionService.updateSessionGroup(sessionId, groupId); - await get().refreshSessions(); - }, useFetchSessions: () => - useSWR(FETCH_SESSIONS_KEY, sessionService.getSessions, { + useSWR(FETCH_SESSIONS_KEY, sessionService.getSessionsWithGroup, { onSuccess: (data) => { // 由于 https://github.com/lobehub/lobe-chat/pull/541 的关系 // 只有触发了 refreshSessions 才会更新 sessions,进而触发页面 rerender - // 因此这里不能补充判断,否则会导致页面不更新 + // 因此这里不能补充 equal 判断,否则会导致页面不更新 + // if (get().isSessionsFirstFetchFinished && isEqual(get().sessions, data)) return; + // TODO:后续的根本解法应该是解除 inbox 和 session 的数据耦合 // 避免互相依赖的情况出现 - // if (get().isSessionsFirstFetchFinished && isEqual(get().sessions, data)) return; - set( { + customSessionGroups: data.customGroup, + defaultSessions: data.default, isSessionsFirstFetchFinished: true, - sessions: data, + pinnedSessions: data.pinned, + sessions: data.all, }, false, n('useFetchSessions/onSuccess', data), diff --git a/src/store/session/slices/session/initialState.ts b/src/store/session/slices/session/initialState.ts index 631fe75c41e7d..f4fc08ea28a4e 100644 --- a/src/store/session/slices/session/initialState.ts +++ b/src/store/session/slices/session/initialState.ts @@ -1,6 +1,8 @@ +import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'; + import { DEFAULT_AGENT_META } from '@/const/meta'; import { DEFAULT_AGENT_CONFIG } from '@/const/settings'; -import { LobeAgentSession, LobeSessionType } from '@/types/session'; +import { CustomSessionGroup, LobeAgentSession, LobeSessionType } from '@/types/session'; export const initLobeSession: LobeAgentSession = { config: DEFAULT_AGENT_CONFIG, @@ -10,3 +12,41 @@ export const initLobeSession: LobeAgentSession = { type: LobeSessionType.Agent, updatedAt: Date.now(), }; + +export interface SessionState { + /** + * @title 当前活动的会话 + * @description 当前正在编辑或查看的会话 + */ + activeId: string; + customSessionGroups: CustomSessionGroup[]; + defaultSessions: LobeAgentSession[]; + isMobile?: boolean; + isSearching: boolean; + isSessionsFirstFetchFinished: boolean; + pinnedSessions: LobeAgentSession[]; + /** + * 后续看看是否可以将 router 部分的逻辑移出去 + * @deprecated + */ + router?: AppRouterInstance; + searchKeywords: string; + searchSessions: LobeAgentSession[]; + /** + * it means defaultSessions + */ + sessions: LobeAgentSession[]; +} + +export const initialSessionState: SessionState = { + activeId: 'inbox', + customSessionGroups: [], + defaultSessions: [], + isMobile: false, + isSearching: false, + isSessionsFirstFetchFinished: false, + pinnedSessions: [], + searchKeywords: '', + searchSessions: [], + sessions: [], +}; diff --git a/src/store/session/slices/session/selectors/export.ts b/src/store/session/slices/session/selectors/export.ts deleted file mode 100644 index a9ae6e1603c36..0000000000000 --- a/src/store/session/slices/session/selectors/export.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { transform } from 'lodash-es'; - -import { SessionStore } from '@/store/session'; -import { ConfigStateAgents, ConfigStateSessions } from '@/types/exportConfig'; -import { LobeAgentSession, LobeSessions } from '@/types/session'; - -import { sessionHelpers } from '../helpers'; - -const exportSessions = (s: SessionStore): Pick => ({ - sessions: s.sessions, -}); - -const exportAgents = (s: SessionStore): ConfigStateAgents => { - return { - sessions: transform(s.sessions, (result: LobeSessions, value, key) => { - // 移除 chats 和 topics - result[key] = { ...value, chats: {}, topics: {} } as LobeAgentSession; - }), - }; -}; - -const getExportAgent = - (id: string) => - (s: SessionStore): LobeAgentSession => - sessionHelpers.getSessionById(id, s.sessions); - -export const sessionExportSelectors = { - exportAgents, - exportSessions, - getExportAgent, -}; diff --git a/src/store/session/slices/session/selectors/index.ts b/src/store/session/slices/session/selectors/index.ts index a1e9d2a81286f..71825137f4685 100644 --- a/src/store/session/slices/session/selectors/index.ts +++ b/src/store/session/slices/session/selectors/index.ts @@ -1,2 +1 @@ -export * from './export'; export * from './list'; diff --git a/src/store/session/slices/session/selectors/list.ts b/src/store/session/slices/session/selectors/list.ts index b2703904fa307..dddb149d21ad6 100644 --- a/src/store/session/slices/session/selectors/list.ts +++ b/src/store/session/slices/session/selectors/list.ts @@ -1,54 +1,23 @@ import { INBOX_SESSION_ID } from '@/const/session'; -import { groupHelpers } from '@/store/global/helpers'; import { sessionHelpers } from '@/store/session/slices/session/helpers'; import { MetaData } from '@/types/meta'; -import { - LobeAgentSession, - LobeSessions, - SessionDefaultGroup, - SessionGroupItem, -} from '@/types/session'; +import { CustomSessionGroup, LobeAgentSession, LobeSessions } from '@/types/session'; import { SessionStore } from '../../../store'; import { initLobeSession } from '../initialState'; -const defaultSessions = (s: SessionStore): LobeSessions => s.sessions; +const defaultSessions = (s: SessionStore): LobeSessions => s.defaultSessions; +const pinnedSessions = (s: SessionStore): LobeSessions => s.pinnedSessions; +const customSessionGroups = (s: SessionStore): CustomSessionGroup[] => s.customSessionGroups; -const searchSessions = (s: SessionStore): LobeSessions => s.searchSessions; - -const sessionList = (sessionCustomGroups: SessionGroupItem[]) => (s: SessionStore) => { - const customList: Record = {}; - const defaultList: LobeAgentSession[] = []; - const pinnedList: LobeAgentSession[] = []; - - for (const session of defaultSessions(s)) { - if (!session.group || session.group === SessionDefaultGroup.Default) { - defaultList.push(session); - } else if (session.group === SessionDefaultGroup.Pinned) { - pinnedList.push(session); - } else { - const group = groupHelpers.getGroupById(session.group, sessionCustomGroups); - if (!group) { - s.updateSessionGroup(session.id, SessionDefaultGroup.Default); - defaultList.push(session); - continue; - } - customList[session.group] = customList[session.group] || []; - customList[session.group].push(session); - } - } +const allSessions = (s: SessionStore): LobeSessions => s.sessions; - return { - customList, - defaultList, - pinnedList, - }; -}; +const searchSessions = (s: SessionStore): LobeSessions => s.searchSessions; const getSessionById = (id: string) => (s: SessionStore): LobeAgentSession => - sessionHelpers.getSessionById(id, s.sessions); + sessionHelpers.getSessionById(id, allSessions(s)); const getSessionMetaById = (id: string) => @@ -62,7 +31,7 @@ const getSessionMetaById = const currentSession = (s: SessionStore): LobeAgentSession | undefined => { if (!s.activeId) return; - return s.sessions.find((i) => i.id === s.activeId); + return allSessions(s).find((i) => i.id === s.activeId); }; const currentSessionSafe = (s: SessionStore): LobeAgentSession => { @@ -81,12 +50,14 @@ const isSomeSessionActive = (s: SessionStore) => !!s.activeId && isSessionListIn export const sessionSelectors = { currentSession, currentSessionSafe, + customSessionGroups, + defaultSessions, getSessionById, getSessionMetaById, hasCustomAgents, isInboxSession, isSessionListInit, isSomeSessionActive, + pinnedSessions, searchSessions, - sessionList, }; diff --git a/src/store/session/slices/sessionGroup/action.ts b/src/store/session/slices/sessionGroup/action.ts new file mode 100644 index 0000000000000..99a8dcdf0cafa --- /dev/null +++ b/src/store/session/slices/sessionGroup/action.ts @@ -0,0 +1,47 @@ +import { StateCreator } from 'zustand/vanilla'; + +import { sessionService } from '@/services/session'; +import { SessionStore } from '@/store/session'; +import { SessionGroupItem } from '@/types/session'; + +export interface SessionGroupAction { + addSessionGroup: (name: string) => Promise; + removeSessionGroup: (id: string) => Promise; + updateSessionGroupId: (sessionId: string, groupId: string) => Promise; + updateSessionGroupName: (id: string, name: string) => Promise; + updateSessionGroupSort: (items: SessionGroupItem[]) => Promise; +} + +export const createSessionGroupSlice: StateCreator< + SessionStore, + [['zustand/devtools', never]], + [], + SessionGroupAction +> = (set, get) => ({ + addSessionGroup: async (name) => { + const id = await sessionService.createSessionGroup(name); + + await get().refreshSessions(); + + return id; + }, + removeSessionGroup: async (id) => { + await sessionService.removeSessionGroup(id); + await get().refreshSessions(); + }, + updateSessionGroupId: async (sessionId, groupId) => { + await sessionService.updateSessionGroupId(sessionId, groupId); + + await get().refreshSessions(); + }, + updateSessionGroupName: async (id, name) => { + await sessionService.updateSessionGroup(id, { name }); + await get().refreshSessions(); + }, + + updateSessionGroupSort: async (items) => { + const sortMap = items.map((item, index) => ({ id: item.id, sort: index })); + await sessionService.updateSessionGroupOrder(sortMap); + await get().refreshSessions(); + }, +}); diff --git a/src/store/session/slices/sessionGroup/initialState.ts b/src/store/session/slices/sessionGroup/initialState.ts new file mode 100644 index 0000000000000..77b7a8c2e3bc3 --- /dev/null +++ b/src/store/session/slices/sessionGroup/initialState.ts @@ -0,0 +1,5 @@ +export interface SessionGroupState { + activeGroupId?: string; +} + +export const initSessionGroupState: SessionGroupState = {}; diff --git a/src/store/session/slices/sessionGroup/selectors.ts b/src/store/session/slices/sessionGroup/selectors.ts new file mode 100644 index 0000000000000..8dbb09e5ab473 --- /dev/null +++ b/src/store/session/slices/sessionGroup/selectors.ts @@ -0,0 +1,15 @@ +import { SessionStore } from '@/store/session'; + +const sessionGroupItems = (s: SessionStore) => + s.customSessionGroups.map((group) => ({ + id: group.id, + name: group.name, + })); + +const getGroupById = (id: string) => (s: SessionStore) => + sessionGroupItems(s).find((group) => group.id === id); + +export const sessionGroupSelectors = { + getGroupById, + sessionGroupItems, +}; diff --git a/src/store/session/store.ts b/src/store/session/store.ts index 9e08bf4287873..f79d2ec6ddc6c 100644 --- a/src/store/session/store.ts +++ b/src/store/session/store.ts @@ -8,17 +8,24 @@ import { isDev } from '@/utils/env'; import { SessionStoreState, initialState } from './initialState'; import { AgentAction, createAgentSlice } from './slices/agent/action'; import { SessionAction, createSessionSlice } from './slices/session/action'; +import { SessionGroupAction, createSessionGroupSlice } from './slices/sessionGroup/action'; // =============== 聚合 createStoreFn ============ // -export type SessionStore = SessionAction & AgentAction & SessionStoreState; +export interface SessionStore + extends SessionAction, + AgentAction, + SessionGroupAction, + SessionStoreState {} + const createStore: StateCreator = (...parameters) => ({ ...initialState, ...createAgentSlice(...parameters), ...createSessionSlice(...parameters), + ...createSessionGroupSlice(...parameters), }); -// =============== 实装 useStore ============ // +// =============== implement useStore ============ // export const useSessionStore = createWithEqualityFn()( subscribeWithSelector( diff --git a/src/types/session.ts b/src/types/session.ts index b33cc6b0f9c64..627905fe3afa9 100644 --- a/src/types/session.ts +++ b/src/types/session.ts @@ -15,10 +15,15 @@ export enum SessionDefaultGroup { } export interface SessionGroupItem { - id: SessionGroupId; + createdAt: number; + id: string; name: string; + sort?: number; + updatedAt: number; } +export type SessionGroups = SessionGroupItem[]; + /** * Lobe Agent */ @@ -37,3 +42,16 @@ export interface LobeAgentSettings { } export type LobeSessions = LobeAgentSession[]; + +export interface CustomSessionGroup { + children: LobeSessions; + id: SessionGroupId; + name: string; +} + +export interface ChatSessionList { + all: LobeSessions; + customGroup: CustomSessionGroup[]; + default: LobeSessions; + pinned: LobeSessions; +} diff --git a/src/types/settings.ts b/src/types/settings.ts index 2ea21473302d7..23dc775cbe390 100644 --- a/src/types/settings.ts +++ b/src/types/settings.ts @@ -3,7 +3,6 @@ import type { ThemeMode } from 'antd-style'; import { LocaleMode } from '@/types/locale'; import type { LobeAgentSession } from '@/types/session'; -import { SessionGroupItem } from '@/types/session'; export interface GlobalBaseSettings { avatar: string; @@ -12,7 +11,6 @@ export interface GlobalBaseSettings { neutralColor?: NeutralColors; password: string; primaryColor?: PrimaryColors; - sessionCustomGroups: SessionGroupItem[]; themeMode: ThemeMode; }