@lobehub/chat 0.120.3 → 0.120.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +9 -9
- package/README.zh-CN.md +14 -14
- package/docs/Home.md +2 -2
- package/docs/_Sidebar.md +3 -3
- package/package.json +3 -2
- package/src/app/chat/(desktop)/features/ChatHeader.tsx +6 -6
- package/src/app/chat/{features → (desktop)/features/SideBar}/SystemRole/index.tsx +8 -5
- package/src/app/chat/(desktop)/features/{SideBar.tsx → SideBar/index.tsx} +2 -3
- package/src/app/chat/(mobile)/index.tsx +17 -4
- package/src/app/chat/(mobile)/mobile/page.tsx +30 -2
- package/src/app/chat/components/SessionHydration/index.tsx +44 -0
- package/src/app/chat/features/SessionListContent/DefaultMode.tsx +6 -1
- package/src/app/chat/features/SessionListContent/List/index.tsx +7 -3
- package/src/app/chat/features/TopicListContent/Header.tsx +1 -1
- package/src/app/chat/page.tsx +7 -3
- package/src/app/home/Redirect.tsx +2 -2
- package/src/app/metadata.ts +6 -0
- package/src/app/welcome/features/Banner/index.tsx +6 -7
- package/src/components/StoreHydration/ChatHydration/index.tsx +33 -0
- package/src/config/server.ts +4 -0
- package/src/const/url.ts +1 -1
- package/src/database/models/__tests__/message.test.ts +3 -2
- package/src/database/models/message.ts +13 -9
- package/src/database/schemas/message.ts +1 -1
- package/src/features/ChatInput/useChatInput.ts +2 -6
- package/src/features/Conversation/hooks/useInitConversation.ts +0 -2
- package/src/features/Conversation/index.tsx +7 -2
- package/src/features/SideBar/TopActions.tsx +5 -5
- package/src/layout/GlobalLayout/StoreHydration.tsx +12 -11
- package/src/store/chat/slices/message/action.ts +1 -1
- package/src/store/chat/store.ts +2 -34
- package/src/store/global/slices/common/action.ts +6 -0
- package/src/store/global/slices/common/initialState.ts +5 -0
- package/src/store/session/index.ts +0 -1
- package/src/store/session/initialState.ts +6 -2
- package/src/store/session/slices/agent/selectors.ts +3 -1
- package/src/store/session/slices/session/action.ts +13 -17
- package/src/store/session/slices/session/selectors/list.ts +7 -0
- package/src/store/session/store.ts +5 -46
- package/src/app/chat/(mobile)/layout.mobile.tsx +0 -17
- package/src/app/chat/(mobile)/mobile/index.tsx +0 -30
- package/src/app/chat/(mobile)/mobile/layout.mobile.tsx +0 -11
- package/src/store/session/hooks/index.ts +0 -4
- package/src/store/session/hooks/useEffectAfterHydrated.ts +0 -27
- package/src/store/session/hooks/useOnFinishHydrationSession.ts +0 -19
- package/src/store/session/hooks/useSessionChatInit.ts +0 -12
- package/src/store/session/hooks/useSessionHydrated.ts +0 -13
- /package/docs/Development/{index.md → Intro.md} +0 -0
- /package/docs/Development/{index.zh-CN.md → Intro.zh-CN.md} +0 -0
- /package/docs/Development/State-Management/{index.md → State-Management-Intro.md} +0 -0
- /package/docs/Development/State-Management/{index.zh-CN.md → State-Management-Intro.zh-CN.md} +0 -0
- /package/src/app/chat/{features → (desktop)/features/SideBar}/SystemRole/style.ts +0 -0
- /package/src/app/chat/{features → components}/SidebarHeader/index.tsx +0 -0
package/src/config/server.ts
CHANGED
|
@@ -16,6 +16,8 @@ declare global {
|
|
|
16
16
|
|
|
17
17
|
IMGUR_CLIENT_ID?: string;
|
|
18
18
|
|
|
19
|
+
METADATA_BASE_URL?: string;
|
|
20
|
+
|
|
19
21
|
AGENTS_INDEX_URL?: string;
|
|
20
22
|
|
|
21
23
|
PLUGINS_INDEX_URL?: string;
|
|
@@ -51,6 +53,8 @@ export const getServerConfig = () => {
|
|
|
51
53
|
OPENAI_PROXY_URL: process.env.OPENAI_PROXY_URL,
|
|
52
54
|
OPENAI_FUNCTION_REGIONS: regions,
|
|
53
55
|
|
|
56
|
+
METADATA_BASE_URL: process.env.METADATA_BASE_URL,
|
|
57
|
+
|
|
54
58
|
AZURE_API_KEY: process.env.AZURE_API_KEY,
|
|
55
59
|
AZURE_API_VERSION: process.env.AZURE_API_VERSION,
|
|
56
60
|
USE_AZURE_OPENAI: process.env.USE_AZURE_OPENAI === '1',
|
package/src/const/url.ts
CHANGED
|
@@ -17,5 +17,5 @@ export const AGENTS_INDEX_GITHUB = 'https://github.com/lobehub/lobe-chat-agents'
|
|
|
17
17
|
export const AGENTS_INDEX_GITHUB_ISSUE = urlJoin(AGENTS_INDEX_GITHUB, 'issues/new');
|
|
18
18
|
|
|
19
19
|
export const SESSION_CHAT_URL = (id: string = INBOX_SESSION_ID, mobile?: boolean) =>
|
|
20
|
-
mobile ? `/chat/mobile
|
|
20
|
+
mobile ? `/chat/mobile?session=${id}` : `/chat?session=${id}`;
|
|
21
21
|
export const MANUAL_UPGRADE_URL = 'https://github.com/lobehub/lobe-chat/wiki/Upstream-Sync';
|
|
@@ -329,14 +329,15 @@ describe('MessageModel', () => {
|
|
|
329
329
|
describe('duplicateMessages', () => {
|
|
330
330
|
it('should duplicate messages and update parentId for copied messages', async () => {
|
|
331
331
|
// 创建原始消息和父消息
|
|
332
|
-
const parentMessageData:
|
|
332
|
+
const parentMessageData: CreateMessageParams = {
|
|
333
333
|
content: 'Parent message content',
|
|
334
334
|
role: 'user',
|
|
335
335
|
sessionId: 'session1',
|
|
336
|
+
topicId: undefined,
|
|
336
337
|
};
|
|
337
338
|
const parentMessage = await MessageModel.create(parentMessageData);
|
|
338
339
|
|
|
339
|
-
const childMessageData:
|
|
340
|
+
const childMessageData: CreateMessageParams = {
|
|
340
341
|
content: 'Child message content',
|
|
341
342
|
role: 'user',
|
|
342
343
|
sessionId: 'session1',
|
|
@@ -46,14 +46,13 @@ class _MessageModel extends BaseModel {
|
|
|
46
46
|
}: QueryMessageParams): Promise<ChatMessage[]> {
|
|
47
47
|
const offset = current * pageSize;
|
|
48
48
|
|
|
49
|
-
const query =
|
|
50
|
-
topicId
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.and((message) => !message.topicId);
|
|
49
|
+
const query = !!topicId
|
|
50
|
+
? // TODO: The query {"sessionId":"xxx","topicId":"xxx"} on messages would benefit of a compound index [sessionId+topicId]
|
|
51
|
+
this.table.where({ sessionId, topicId }) // Use a compound index
|
|
52
|
+
: this.table
|
|
53
|
+
.where('sessionId')
|
|
54
|
+
.equals(sessionId)
|
|
55
|
+
.and((message) => !message.topicId);
|
|
57
56
|
|
|
58
57
|
const dbMessages: DBModel<DB_Message>[] = await query
|
|
59
58
|
.sortBy('createdAt')
|
|
@@ -221,7 +220,12 @@ class _MessageModel extends BaseModel {
|
|
|
221
220
|
tts,
|
|
222
221
|
...item
|
|
223
222
|
}: DBModel<DB_Message>): ChatMessage => {
|
|
224
|
-
return {
|
|
223
|
+
return {
|
|
224
|
+
...item,
|
|
225
|
+
extra: { fromModel, translate, tts },
|
|
226
|
+
meta: {},
|
|
227
|
+
topicId: item.topicId ?? undefined,
|
|
228
|
+
};
|
|
225
229
|
};
|
|
226
230
|
}
|
|
227
231
|
|
|
@@ -31,7 +31,7 @@ export const DB_MessageSchema = z.object({
|
|
|
31
31
|
parentId: z.string().optional(),
|
|
32
32
|
quotaId: z.string().optional(),
|
|
33
33
|
sessionId: z.string(),
|
|
34
|
-
topicId: z.string().optional(),
|
|
34
|
+
topicId: z.string().nullable().optional(),
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
export type DB_Message = z.infer<typeof DB_MessageSchema>;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { TextAreaRef } from 'antd/es/input/TextArea';
|
|
2
2
|
import { useCallback, useRef, useState } from 'react';
|
|
3
3
|
|
|
4
|
-
import { useIsMobile } from '@/hooks/useIsMobile';
|
|
5
4
|
import { useChatStore } from '@/store/chat';
|
|
6
5
|
import { useSessionStore } from '@/store/session';
|
|
7
6
|
import { agentSelectors } from '@/store/session/selectors';
|
|
@@ -20,15 +19,12 @@ export const useChatInput = () => {
|
|
|
20
19
|
s.updateInputMessage,
|
|
21
20
|
s.stopGenerateMessage,
|
|
22
21
|
]);
|
|
23
|
-
const mobile = useIsMobile();
|
|
24
22
|
|
|
25
23
|
const handleSend = useCallback(() => {
|
|
26
24
|
setExpand(false);
|
|
27
|
-
|
|
28
|
-
ref?.current?.blur();
|
|
29
|
-
}
|
|
25
|
+
|
|
30
26
|
onSend();
|
|
31
|
-
}, [onSend
|
|
27
|
+
}, [onSend]);
|
|
32
28
|
|
|
33
29
|
return {
|
|
34
30
|
canUpload,
|
|
@@ -28,8 +28,6 @@ export const useInitConversation = () => {
|
|
|
28
28
|
checkPluginsIsInstalled(plugins);
|
|
29
29
|
|
|
30
30
|
useEffect(() => {
|
|
31
|
-
useChatStore.persist.rehydrate();
|
|
32
|
-
|
|
33
31
|
// // when activeId changed, switch topic to undefined
|
|
34
32
|
const unsubscribe = useSessionStore.subscribe(
|
|
35
33
|
(s) => s.activeId,
|
|
@@ -2,6 +2,7 @@ import { createStyles } from 'antd-style';
|
|
|
2
2
|
import { ReactNode, memo } from 'react';
|
|
3
3
|
import { Flexbox } from 'react-layout-kit';
|
|
4
4
|
|
|
5
|
+
import ChatHydration from '@/components/StoreHydration/ChatHydration';
|
|
5
6
|
import { useGlobalStore } from '@/store/global';
|
|
6
7
|
import { settingsSelectors } from '@/store/global/selectors';
|
|
7
8
|
|
|
@@ -50,12 +51,16 @@ const Conversation = memo<ConversationProps>(({ chatInput, mobile }) => {
|
|
|
50
51
|
const init = useInitConversation();
|
|
51
52
|
|
|
52
53
|
return (
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
<Flexbox
|
|
55
|
+
flex={1}
|
|
56
|
+
// position: 'relative' is required, ChatInput's absolute position needs it
|
|
57
|
+
style={{ position: 'relative' }}
|
|
58
|
+
>
|
|
55
59
|
<div className={styles}>
|
|
56
60
|
{init ? <ChatList mobile={mobile} /> : <SkeletonList mobile={mobile} />}
|
|
57
61
|
</div>
|
|
58
62
|
{chatInput}
|
|
63
|
+
<ChatHydration />
|
|
59
64
|
</Flexbox>
|
|
60
65
|
);
|
|
61
66
|
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ActionIcon } from '@lobehub/ui';
|
|
2
|
-
import {
|
|
2
|
+
import { Compass, MessageSquare } from 'lucide-react';
|
|
3
3
|
import Link from 'next/link';
|
|
4
4
|
import { memo } from 'react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
|
|
7
|
-
import { GlobalStore } from '@/store/global';
|
|
7
|
+
import { GlobalStore, useGlobalStore } from '@/store/global';
|
|
8
8
|
import { SidebarTabKey } from '@/store/global/initialState';
|
|
9
9
|
import { useSessionStore } from '@/store/session';
|
|
10
10
|
|
|
@@ -15,7 +15,7 @@ export interface TopActionProps {
|
|
|
15
15
|
|
|
16
16
|
const TopActions = memo<TopActionProps>(({ tab, setTab }) => {
|
|
17
17
|
const { t } = useTranslation('common');
|
|
18
|
-
const switchBackToChat =
|
|
18
|
+
const switchBackToChat = useGlobalStore((s) => s.switchBackToChat);
|
|
19
19
|
|
|
20
20
|
return (
|
|
21
21
|
<>
|
|
@@ -23,7 +23,7 @@ const TopActions = memo<TopActionProps>(({ tab, setTab }) => {
|
|
|
23
23
|
href={'/chat'}
|
|
24
24
|
onClick={(e) => {
|
|
25
25
|
e.preventDefault();
|
|
26
|
-
switchBackToChat();
|
|
26
|
+
switchBackToChat(useSessionStore.getState().activeId);
|
|
27
27
|
setTab(SidebarTabKey.Chat);
|
|
28
28
|
}}
|
|
29
29
|
>
|
|
@@ -38,7 +38,7 @@ const TopActions = memo<TopActionProps>(({ tab, setTab }) => {
|
|
|
38
38
|
<Link href={'/market'}>
|
|
39
39
|
<ActionIcon
|
|
40
40
|
active={tab === SidebarTabKey.Market}
|
|
41
|
-
icon={
|
|
41
|
+
icon={Compass}
|
|
42
42
|
onClick={() => {
|
|
43
43
|
setTab(SidebarTabKey.Market);
|
|
44
44
|
}}
|
|
@@ -2,32 +2,32 @@ import { useResponsive } from 'antd-style';
|
|
|
2
2
|
import { useRouter } from 'next/navigation';
|
|
3
3
|
import { memo, useEffect } from 'react';
|
|
4
4
|
|
|
5
|
-
import { useGlobalStore } from '@/store/global';
|
|
6
|
-
import { useEffectAfterSessionHydrated, useSessionStore } from '@/store/session';
|
|
5
|
+
import { useEffectAfterGlobalHydrated, useGlobalStore } from '@/store/global';
|
|
7
6
|
|
|
8
7
|
const StoreHydration = memo(() => {
|
|
9
|
-
const router = useRouter();
|
|
10
|
-
|
|
11
8
|
const useFetchGlobalConfig = useGlobalStore((s) => s.useFetchGlobalConfig);
|
|
9
|
+
useFetchGlobalConfig();
|
|
12
10
|
|
|
13
11
|
useEffect(() => {
|
|
14
12
|
// refs: https://github.com/pmndrs/zustand/blob/main/docs/integrations/persisting-store-data.md#hashydrated
|
|
15
|
-
useSessionStore.persist.rehydrate();
|
|
16
13
|
useGlobalStore.persist.rehydrate();
|
|
17
14
|
}, []);
|
|
18
15
|
|
|
19
|
-
useFetchGlobalConfig();
|
|
20
|
-
|
|
21
16
|
const { mobile } = useResponsive();
|
|
22
|
-
|
|
23
|
-
useEffectAfterSessionHydrated(
|
|
17
|
+
useEffectAfterGlobalHydrated(
|
|
24
18
|
(store) => {
|
|
25
|
-
store.
|
|
19
|
+
const prevState = store.getState().isMobile;
|
|
20
|
+
|
|
21
|
+
if (prevState !== mobile) {
|
|
22
|
+
store.setState({ isMobile: mobile });
|
|
23
|
+
}
|
|
26
24
|
},
|
|
27
25
|
[mobile],
|
|
28
26
|
);
|
|
29
27
|
|
|
30
|
-
|
|
28
|
+
const router = useRouter();
|
|
29
|
+
|
|
30
|
+
useEffectAfterGlobalHydrated(
|
|
31
31
|
(store) => {
|
|
32
32
|
store.setState({ router });
|
|
33
33
|
},
|
|
@@ -36,6 +36,7 @@ const StoreHydration = memo(() => {
|
|
|
36
36
|
|
|
37
37
|
useEffect(() => {
|
|
38
38
|
router.prefetch('/chat');
|
|
39
|
+
router.prefetch('/chat/settings');
|
|
39
40
|
router.prefetch('/market');
|
|
40
41
|
router.prefetch('/settings/common');
|
|
41
42
|
router.prefetch('/settings/agent');
|
|
@@ -168,7 +168,7 @@ export const chatMessage: StateCreator<
|
|
|
168
168
|
// if message is empty and no files, then stop
|
|
169
169
|
if (!message && (!fileIdList || fileIdList?.length === 0)) return;
|
|
170
170
|
|
|
171
|
-
|
|
171
|
+
const newMessage: CreateMessageParams = {
|
|
172
172
|
content: message,
|
|
173
173
|
// if message has attached with files, then add files to message and the agent
|
|
174
174
|
files: fileIdList,
|
package/src/store/chat/store.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { devtools, subscribeWithSelector } from 'zustand/middleware';
|
|
2
2
|
import { shallow } from 'zustand/shallow';
|
|
3
3
|
import { createWithEqualityFn } from 'zustand/traditional';
|
|
4
4
|
import { StateCreator } from 'zustand/vanilla';
|
|
5
5
|
|
|
6
|
-
import { createHyperStorage } from '@/store/middleware/createHyperStorage';
|
|
7
6
|
import { isDev } from '@/utils/env';
|
|
8
|
-
import { isInStandaloneMode } from '@/utils/matchMedia';
|
|
9
|
-
import { isMobileScreen } from '@/utils/screen';
|
|
10
7
|
|
|
11
8
|
import { ChatStoreState, initialState } from './initialState';
|
|
12
9
|
import { ChatEnhanceAction, chatEnhance } from './slices/enchance/action';
|
|
@@ -36,42 +33,13 @@ const createStore: StateCreator<ChatStore, [['zustand/devtools', never]]> = (...
|
|
|
36
33
|
...chatPlugin(...params),
|
|
37
34
|
});
|
|
38
35
|
|
|
39
|
-
const persistOptions: PersistOptions<ChatStore> = {
|
|
40
|
-
name: 'LobeChat_Chat',
|
|
41
|
-
|
|
42
|
-
// 手动控制 Hydration ,避免 ssr 报错
|
|
43
|
-
skipHydration: true,
|
|
44
|
-
|
|
45
|
-
storage: createHyperStorage(() => {
|
|
46
|
-
const isMobileAndPWA = isMobileScreen() && isInStandaloneMode();
|
|
47
|
-
const selectors = [{ activeTopicId: 'topic' }];
|
|
48
|
-
|
|
49
|
-
if (isMobileAndPWA)
|
|
50
|
-
return {
|
|
51
|
-
localStorage: {
|
|
52
|
-
selectors,
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
localStorage: false,
|
|
58
|
-
url: {
|
|
59
|
-
mode: 'hash',
|
|
60
|
-
selectors,
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}),
|
|
64
|
-
version: 0,
|
|
65
|
-
};
|
|
66
|
-
|
|
67
36
|
// =============== 实装 useStore ============ //
|
|
68
37
|
|
|
69
38
|
export const useChatStore = createWithEqualityFn<ChatStore>()(
|
|
70
|
-
|
|
39
|
+
subscribeWithSelector(
|
|
71
40
|
devtools(createStore, {
|
|
72
41
|
name: 'LobeChat_Chat' + (isDev ? '_DEV' : ''),
|
|
73
42
|
}),
|
|
74
|
-
persistOptions,
|
|
75
43
|
),
|
|
76
44
|
shallow,
|
|
77
45
|
);
|
|
@@ -3,6 +3,8 @@ import { gt } from 'semver';
|
|
|
3
3
|
import useSWR, { SWRResponse } from 'swr';
|
|
4
4
|
import type { StateCreator } from 'zustand/vanilla';
|
|
5
5
|
|
|
6
|
+
import { INBOX_SESSION_ID } from '@/const/session';
|
|
7
|
+
import { SESSION_CHAT_URL } from '@/const/url';
|
|
6
8
|
import { CURRENT_VERSION } from '@/const/version';
|
|
7
9
|
import { globalService } from '@/services/global';
|
|
8
10
|
import type { GlobalStore } from '@/store/global';
|
|
@@ -18,6 +20,7 @@ const n = setNamespace('settings');
|
|
|
18
20
|
* 设置操作
|
|
19
21
|
*/
|
|
20
22
|
export interface CommonAction {
|
|
23
|
+
switchBackToChat: (sessionId?: string) => void;
|
|
21
24
|
/**
|
|
22
25
|
* 切换侧边栏选项
|
|
23
26
|
* @param key - 选中的侧边栏选项
|
|
@@ -38,6 +41,9 @@ export const createCommonSlice: StateCreator<
|
|
|
38
41
|
[],
|
|
39
42
|
CommonAction
|
|
40
43
|
> = (set, get) => ({
|
|
44
|
+
switchBackToChat: (sessionId) => {
|
|
45
|
+
get().router?.push(SESSION_CHAT_URL(sessionId || INBOX_SESSION_ID, get().isMobile));
|
|
46
|
+
},
|
|
41
47
|
switchSideBar: (key) => {
|
|
42
48
|
set({ sidebarKey: key }, false, n('switchSideBar', key));
|
|
43
49
|
},
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
|
|
2
|
+
|
|
1
3
|
export enum SidebarTabKey {
|
|
2
4
|
Chat = 'chat',
|
|
3
5
|
Market = 'market',
|
|
@@ -33,17 +35,20 @@ export interface GlobalPreference {
|
|
|
33
35
|
|
|
34
36
|
export interface GlobalCommonState {
|
|
35
37
|
hasNewVersion?: boolean;
|
|
38
|
+
isMobile?: boolean;
|
|
36
39
|
latestVersion?: string;
|
|
37
40
|
/**
|
|
38
41
|
* 用户偏好的 UI 状态
|
|
39
42
|
* @localStorage
|
|
40
43
|
*/
|
|
41
44
|
preference: GlobalPreference;
|
|
45
|
+
router?: AppRouterInstance;
|
|
42
46
|
settingsTab: SettingsTabs;
|
|
43
47
|
sidebarKey: SidebarTabKey;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
export const initialCommonState: GlobalCommonState = {
|
|
51
|
+
isMobile: false,
|
|
47
52
|
preference: {
|
|
48
53
|
guide: {},
|
|
49
54
|
inputHeight: 200,
|
|
@@ -10,9 +10,13 @@ export interface SessionState {
|
|
|
10
10
|
* @description 当前正在编辑或查看的会话
|
|
11
11
|
*/
|
|
12
12
|
activeId: string;
|
|
13
|
-
fetchSessionsLoading: boolean;
|
|
14
13
|
isMobile?: boolean;
|
|
15
14
|
isSearching: boolean;
|
|
15
|
+
isSessionsFirstFetchFinished: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* 后续看看是否可以将 router 部分的逻辑移出去
|
|
18
|
+
* @deprecated
|
|
19
|
+
*/
|
|
16
20
|
router?: AppRouterInstance;
|
|
17
21
|
searchKeywords: string;
|
|
18
22
|
searchSessions: LobeAgentSession[];
|
|
@@ -21,9 +25,9 @@ export interface SessionState {
|
|
|
21
25
|
|
|
22
26
|
export const initialState: SessionStoreState = {
|
|
23
27
|
activeId: 'inbox',
|
|
24
|
-
fetchSessionsLoading: true,
|
|
25
28
|
isMobile: false,
|
|
26
29
|
isSearching: false,
|
|
30
|
+
isSessionsFirstFetchFinished: false,
|
|
27
31
|
searchKeywords: '',
|
|
28
32
|
searchSessions: [],
|
|
29
33
|
sessions: [],
|
|
@@ -78,7 +78,9 @@ const currentAgentMeta = (s: SessionStore): MetaData => {
|
|
|
78
78
|
const defaultMeta = {
|
|
79
79
|
avatar: isInbox ? DEFAULT_INBOX_AVATAR : DEFAULT_AVATAR,
|
|
80
80
|
backgroundColor: DEFAULT_BACKGROUND_COLOR,
|
|
81
|
-
description: isInbox
|
|
81
|
+
description: isInbox
|
|
82
|
+
? t('inbox.desc', { ns: 'chat' })
|
|
83
|
+
: currentAgentSystemRole(s) || t('noDescription'),
|
|
82
84
|
title: isInbox ? t('inbox.title', { ns: 'chat' }) : t('defaultSession'),
|
|
83
85
|
};
|
|
84
86
|
|
|
@@ -46,10 +46,6 @@ export interface SessionAction {
|
|
|
46
46
|
duplicateSession: (id: string) => Promise<void>;
|
|
47
47
|
/**
|
|
48
48
|
* Pins or unpins a session.
|
|
49
|
-
*
|
|
50
|
-
* @param {string} id - The ID of the session to pin or unpin.
|
|
51
|
-
* @param {boolean} [pinned] - Optional. If set to true, it pins the session. If set to false or omitted, it unpins the session.
|
|
52
|
-
* @returns {Promise<void>} A promise that resolves when the session is successfully pinned or unpinned.
|
|
53
49
|
*/
|
|
54
50
|
pinSession: (id: string, pinned?: boolean) => Promise<void>;
|
|
55
51
|
/**
|
|
@@ -61,7 +57,6 @@ export interface SessionAction {
|
|
|
61
57
|
* @param id - sessionId
|
|
62
58
|
*/
|
|
63
59
|
removeSession: (id: string) => void;
|
|
64
|
-
switchBackToChat: () => void;
|
|
65
60
|
/**
|
|
66
61
|
* switch session url
|
|
67
62
|
*/
|
|
@@ -80,7 +75,9 @@ export const createSessionSlice: StateCreator<
|
|
|
80
75
|
SessionAction
|
|
81
76
|
> = (set, get) => ({
|
|
82
77
|
activeSession: (sessionId) => {
|
|
83
|
-
|
|
78
|
+
if (get().activeId === sessionId) return;
|
|
79
|
+
|
|
80
|
+
set({ activeId: sessionId }, false, n(`activeSession/${sessionId}`));
|
|
84
81
|
},
|
|
85
82
|
|
|
86
83
|
clearSessions: async () => {
|
|
@@ -147,30 +144,29 @@ export const createSessionSlice: StateCreator<
|
|
|
147
144
|
get().switchSession();
|
|
148
145
|
}
|
|
149
146
|
},
|
|
150
|
-
|
|
151
|
-
switchBackToChat: () => {
|
|
152
|
-
const { activeId, router } = get();
|
|
153
|
-
|
|
154
|
-
const id = activeId || INBOX_SESSION_ID;
|
|
155
|
-
|
|
156
|
-
get().activeSession(id);
|
|
157
|
-
|
|
158
|
-
router?.push(SESSION_CHAT_URL(id, get().isMobile));
|
|
159
|
-
},
|
|
160
147
|
switchSession: (sessionId = INBOX_SESSION_ID) => {
|
|
161
148
|
const { isMobile, router } = get();
|
|
162
149
|
|
|
163
150
|
get().activeSession(sessionId);
|
|
164
151
|
|
|
152
|
+
// TODO: 后续可以把 router 移除
|
|
165
153
|
router?.push(SESSION_CHAT_URL(sessionId, isMobile));
|
|
166
154
|
},
|
|
167
155
|
|
|
168
156
|
useFetchSessions: () =>
|
|
169
157
|
useSWR<LobeSessions>(FETCH_SESSIONS_KEY, sessionService.getSessions, {
|
|
170
158
|
onSuccess: (data) => {
|
|
159
|
+
// 由于 https://github.com/lobehub/lobe-chat/pull/541 的关系
|
|
160
|
+
// 只有触发了 refreshSessions 才会更新 sessions,进而触发页面 rerender
|
|
161
|
+
// 因此这里不能补充判断,否则会导致页面不更新
|
|
162
|
+
// TODO:后续的根本解法应该是解除 inbox 和 session 的数据耦合
|
|
163
|
+
// 避免互相依赖的情况出现
|
|
164
|
+
|
|
165
|
+
// if (get().isSessionsFirstFetchFinished && isEqual(get().sessions, data)) return;
|
|
166
|
+
|
|
171
167
|
set(
|
|
172
168
|
{
|
|
173
|
-
|
|
169
|
+
isSessionsFirstFetchFinished: true,
|
|
174
170
|
sessions: data,
|
|
175
171
|
},
|
|
176
172
|
false,
|
|
@@ -49,6 +49,11 @@ const hasCustomAgents = (s: SessionStore) => defaultSessions(s).length > 0;
|
|
|
49
49
|
|
|
50
50
|
const isInboxSession = (s: SessionStore) => s.activeId === INBOX_SESSION_ID;
|
|
51
51
|
|
|
52
|
+
const isSessionListInit = (s: SessionStore) => s.isSessionsFirstFetchFinished;
|
|
53
|
+
|
|
54
|
+
// use to judge whether a session is fully activated
|
|
55
|
+
const isSomeSessionActive = (s: SessionStore) => !!s.activeId && isSessionListInit(s);
|
|
56
|
+
|
|
52
57
|
export const sessionSelectors = {
|
|
53
58
|
currentSession,
|
|
54
59
|
currentSessionSafe,
|
|
@@ -57,6 +62,8 @@ export const sessionSelectors = {
|
|
|
57
62
|
hasCustomAgents,
|
|
58
63
|
hasPinnedSessionList,
|
|
59
64
|
isInboxSession,
|
|
65
|
+
isSessionListInit,
|
|
66
|
+
isSomeSessionActive,
|
|
60
67
|
pinnedSessionList,
|
|
61
68
|
searchSessions,
|
|
62
69
|
unpinnedSessionList,
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { devtools, subscribeWithSelector } from 'zustand/middleware';
|
|
2
2
|
import { shallow } from 'zustand/shallow';
|
|
3
3
|
import { createWithEqualityFn } from 'zustand/traditional';
|
|
4
4
|
import { StateCreator } from 'zustand/vanilla';
|
|
5
5
|
|
|
6
6
|
import { isDev } from '@/utils/env';
|
|
7
|
-
import { isInStandaloneMode } from '@/utils/matchMedia';
|
|
8
|
-
import { isMobileScreen } from '@/utils/screen';
|
|
9
7
|
|
|
10
|
-
import { createHyperStorage } from '../middleware/createHyperStorage';
|
|
11
8
|
import { SessionStoreState, initialState } from './initialState';
|
|
12
9
|
import { AgentAction, createAgentSlice } from './slices/agent/action';
|
|
13
10
|
import { SessionAction, createSessionSlice } from './slices/session/action';
|
|
@@ -21,51 +18,13 @@ const createStore: StateCreator<SessionStore, [['zustand/devtools', never]]> = (
|
|
|
21
18
|
...createSessionSlice(...parameters),
|
|
22
19
|
});
|
|
23
20
|
|
|
24
|
-
// =============== persist 本地缓存中间件配置 ============ //
|
|
25
|
-
|
|
26
|
-
const persistOptions: PersistOptions<SessionStore> = {
|
|
27
|
-
name: 'LOBE_CHAT',
|
|
28
|
-
|
|
29
|
-
// 手动控制 Hydration ,避免 ssr 报错
|
|
30
|
-
skipHydration: true,
|
|
31
|
-
|
|
32
|
-
storage: createHyperStorage(() => {
|
|
33
|
-
const isMobileAndPWA = isMobileScreen() && isInStandaloneMode();
|
|
34
|
-
|
|
35
|
-
const selectors = [
|
|
36
|
-
// map state key to storage key
|
|
37
|
-
{ activeId: 'session' },
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
if (isMobileAndPWA)
|
|
41
|
-
return {
|
|
42
|
-
localStorage: {
|
|
43
|
-
selectors,
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
localStorage: false,
|
|
49
|
-
url: {
|
|
50
|
-
mode: 'hash',
|
|
51
|
-
selectors,
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
}),
|
|
55
|
-
|
|
56
|
-
version: 2,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
21
|
// =============== 实装 useStore ============ //
|
|
60
22
|
|
|
61
23
|
export const useSessionStore = createWithEqualityFn<SessionStore>()(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}),
|
|
67
|
-
),
|
|
68
|
-
persistOptions,
|
|
24
|
+
subscribeWithSelector(
|
|
25
|
+
devtools(createStore, {
|
|
26
|
+
name: 'LobeChat_Session' + (isDev ? '_DEV' : ''),
|
|
27
|
+
}),
|
|
69
28
|
),
|
|
70
29
|
shallow,
|
|
71
30
|
);
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { PropsWithChildren, memo } from 'react';
|
|
4
|
-
|
|
5
|
-
import SessionHeader from '@/app/chat/(mobile)/features/SessionHeader';
|
|
6
|
-
import AppLayoutMobile from '@/layout/AppLayout.mobile';
|
|
7
|
-
import { useSwitchSideBarOnInit } from '@/store/global';
|
|
8
|
-
import { SidebarTabKey } from '@/store/global/initialState';
|
|
9
|
-
|
|
10
|
-
export default memo(({ children }: PropsWithChildren) => {
|
|
11
|
-
useSwitchSideBarOnInit(SidebarTabKey.Chat);
|
|
12
|
-
return (
|
|
13
|
-
<AppLayoutMobile navBar={<SessionHeader />} showTabBar>
|
|
14
|
-
{children}
|
|
15
|
-
</AppLayoutMobile>
|
|
16
|
-
);
|
|
17
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import dynamic from 'next/dynamic';
|
|
4
|
-
import { memo } from 'react';
|
|
5
|
-
import { Flexbox } from 'react-layout-kit';
|
|
6
|
-
|
|
7
|
-
import Conversation from '@/features/Conversation';
|
|
8
|
-
import { useSessionStore } from '@/store/session';
|
|
9
|
-
|
|
10
|
-
import ChatInput from '../features/ChatInput';
|
|
11
|
-
import Layout from './layout.mobile';
|
|
12
|
-
|
|
13
|
-
const TopicList = dynamic(() => import('../features/TopicList'));
|
|
14
|
-
|
|
15
|
-
const Chat = memo(() => {
|
|
16
|
-
// due to mobile side don't have sessionList, so we need to fetch sessions here
|
|
17
|
-
// refs: https://github.com/lobehub/lobe-chat/pull/541
|
|
18
|
-
const useFetchSessions = useSessionStore((s) => s.useFetchSessions);
|
|
19
|
-
useFetchSessions();
|
|
20
|
-
|
|
21
|
-
return (
|
|
22
|
-
<Layout>
|
|
23
|
-
<Flexbox height={'calc(100% - 44px)'} horizontal>
|
|
24
|
-
<Conversation chatInput={<ChatInput />} mobile />
|
|
25
|
-
<TopicList />
|
|
26
|
-
</Flexbox>
|
|
27
|
-
</Layout>
|
|
28
|
-
);
|
|
29
|
-
});
|
|
30
|
-
export default Chat;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { PropsWithChildren } from 'react';
|
|
4
|
-
|
|
5
|
-
import AppLayoutMobile from '@/layout/AppLayout.mobile';
|
|
6
|
-
|
|
7
|
-
import ChatHeader from '../features/ChatHeader';
|
|
8
|
-
|
|
9
|
-
export default ({ children }: PropsWithChildren) => (
|
|
10
|
-
<AppLayoutMobile navBar={<ChatHeader />}>{children}</AppLayoutMobile>
|
|
11
|
-
);
|