@lobehub/chat 0.69.0 → 0.70.0

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.
Files changed (48) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.github/workflows/contributor-help.yml +2 -4
  3. package/CHANGELOG.md +50 -0
  4. package/package.json +3 -3
  5. package/src/const/url.ts +3 -0
  6. package/src/features/MobileTabBar/index.tsx +3 -1
  7. package/src/features/SideBar/TopActions.tsx +4 -3
  8. package/src/pages/api/openai.ts +33 -36
  9. package/src/pages/chat/features/Conversation/ChatList/Plugins/IFrameRender/index.tsx +1 -1
  10. package/src/pages/chat/features/Conversation/ChatList/Plugins/IFrameRender/utils.ts +1 -1
  11. package/src/pages/chat/features/Conversation/ChatList/Plugins/SystemJsRender/index.tsx +1 -1
  12. package/src/pages/chat/features/Header/Mobile.tsx +6 -8
  13. package/src/pages/chat/features/Header/PC.tsx +15 -16
  14. package/src/pages/chat/features/Header/index.tsx +3 -3
  15. package/src/pages/chat/{SessionList → features/SessionList}/Inbox.tsx +9 -2
  16. package/src/pages/chat/{SessionList → features/SessionList}/List/index.tsx +10 -1
  17. package/src/pages/chat/features/Sidebar/index.tsx +6 -2
  18. package/src/pages/chat/index.page.tsx +9 -11
  19. package/src/pages/chat/layout.tsx +6 -1
  20. package/src/pages/chat/{[id] → mobile}/index.page.tsx +7 -17
  21. package/src/pages/chat/mobile.tsx +2 -2
  22. package/src/pages/chat/{[id]/setting → setting}/Header.tsx +3 -4
  23. package/src/pages/index.page.tsx +10 -4
  24. package/src/pages/settings/Header.tsx +4 -2
  25. package/src/pages/welcome/Banner/index.tsx +11 -3
  26. package/src/services/chatModel.ts +1 -2
  27. package/src/store/middleware/createHyperStorage/index.ts +95 -0
  28. package/src/store/middleware/createHyperStorage/keyMapper.ts +53 -0
  29. package/src/store/middleware/createHyperStorage/type.ts +14 -0
  30. package/src/store/middleware/createHyperStorage/urlSearch.ts +17 -0
  31. package/src/store/plugin/selectors.ts +1 -1
  32. package/src/store/session/slices/session/action.ts +18 -9
  33. package/src/store/session/slices/session/initialState.ts +2 -3
  34. package/src/store/session/slices/session/selectors/index.ts +6 -0
  35. package/src/store/session/store.ts +16 -8
  36. package/src/types/openai.ts +23 -0
  37. package/src/pages/chat/[id]/layout.tsx +0 -34
  38. package/src/pages/chat/[id]/mobile.tsx +0 -31
  39. package/src/store/middleware/createHashStorage.ts +0 -49
  40. /package/src/pages/chat/{SessionList → features/SessionList}/CollapseGroup.tsx +0 -0
  41. /package/src/pages/chat/{SessionList → features/SessionList}/Header.tsx +0 -0
  42. /package/src/pages/chat/{SessionList → features/SessionList}/List/AddButton.tsx +0 -0
  43. /package/src/pages/chat/{SessionList → features/SessionList}/List/Item/Actions.tsx +0 -0
  44. /package/src/pages/chat/{SessionList → features/SessionList}/List/Item/index.tsx +0 -0
  45. /package/src/pages/chat/{SessionList → features/SessionList}/List/SkeletonList.tsx +0 -0
  46. /package/src/pages/chat/{SessionList → features/SessionList}/index.tsx +0 -0
  47. /package/src/pages/chat/{[id]/setting → setting}/index.page.tsx +0 -0
  48. /package/src/pages/chat/{[id]/setting → setting}/mobile.tsx +0 -0
@@ -7,7 +7,6 @@ import {
7
7
  } from '@lobehub/ui';
8
8
  import { Dropdown, MenuProps } from 'antd';
9
9
  import { HardDriveDownload, Share2 } from 'lucide-react';
10
- import Router from 'next/router';
11
10
  import { memo, useMemo } from 'react';
12
11
  import { useTranslation } from 'react-i18next';
13
12
 
@@ -16,7 +15,7 @@ import { useSessionStore } from '@/store/session';
16
15
 
17
16
  const Header = memo<{ mobile?: boolean }>(({ mobile }) => {
18
17
  const { t } = useTranslation('setting');
19
- const id = useSessionStore((s) => s.activeId);
18
+ const [id, onBack] = useSessionStore((s) => [s.activeId, s.switchBackToChat]);
20
19
 
21
20
  const items = useMemo<MenuProps['items']>(
22
21
  () => [
@@ -46,7 +45,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile }) => {
46
45
  return (
47
46
  <MobileNavBar
48
47
  center={<MobileNavBarTitle title={t('header.session')} />}
49
- onBackClick={() => Router.back()}
48
+ onBackClick={onBack}
50
49
  right={
51
50
  <>
52
51
  <ActionIcon icon={Share2} title={t('share', { ns: 'common' })} />
@@ -62,7 +61,7 @@ const Header = memo<{ mobile?: boolean }>(({ mobile }) => {
62
61
  return (
63
62
  <ChatHeader
64
63
  left={<ChatHeaderTitle title={t('header.session')} />}
65
- onBackClick={() => Router.back()}
64
+ onBackClick={onBack}
66
65
  right={
67
66
  <>
68
67
  <ActionIcon icon={Share2} size={{ fontSize: 24 }} title={t('share', { ns: 'common' })} />
@@ -1,19 +1,25 @@
1
- import { memo } from 'react';
1
+ import { memo, useEffect } from 'react';
2
2
 
3
3
  import { useSessionHydrated, useSessionStore } from '@/store/session';
4
4
  import { sessionSelectors } from '@/store/session/selectors';
5
5
 
6
- import Chat from './chat/index.page';
7
6
  import Loading from './index/Loading';
8
7
  import Welcome from './welcome/index.page';
9
8
 
10
9
  const Home = memo(() => {
11
10
  const hydrated = useSessionHydrated();
12
- const hasSession = useSessionStore(sessionSelectors.hasSessionList);
11
+ const [hasSession, switchSession] = useSessionStore((s) => [
12
+ sessionSelectors.hasSessionList(s),
13
+ s.switchSession,
14
+ ]);
15
+
16
+ useEffect(() => {
17
+ if (hasSession) switchSession();
18
+ }, [hasSession]);
13
19
 
14
20
  if (!hydrated) return <Loading />;
15
21
 
16
- return !hasSession ? <Welcome /> : <Chat />;
22
+ return !hasSession ? <Welcome /> : <Loading />;
17
23
  });
18
24
 
19
25
  export default Home;
@@ -1,15 +1,17 @@
1
1
  import { ChatHeader, ChatHeaderTitle } from '@lobehub/ui';
2
- import Router from 'next/router';
3
2
  import { memo } from 'react';
4
3
  import { useTranslation } from 'react-i18next';
5
4
 
5
+ import { useSessionStore } from '@/store/session';
6
+
6
7
  const Header = memo(() => {
7
8
  const { t } = useTranslation('setting');
9
+ const [switchBackToChat] = useSessionStore((s) => [s.switchBackToChat]);
8
10
 
9
11
  return (
10
12
  <ChatHeader
11
13
  left={<ChatHeaderTitle title={t('header.global')} />}
12
- onBackClick={() => Router.back()}
14
+ onBackClick={switchBackToChat}
13
15
  showBackButton
14
16
  />
15
17
  );
@@ -3,12 +3,13 @@ import { useSize } from 'ahooks';
3
3
  import { Button, Upload } from 'antd';
4
4
  import { SendHorizonal } from 'lucide-react';
5
5
  import Link from 'next/link';
6
- import Router from 'next/router';
7
6
  import { memo, useCallback, useRef } from 'react';
8
7
  import { useTranslation } from 'react-i18next';
9
8
  import { Flexbox } from 'react-layout-kit';
10
9
 
10
+ import { SESSION_CHAT_URL } from '@/const/url';
11
11
  import { useImportConfig } from '@/hooks/useImportConfig';
12
+ import { useSessionStore } from '@/store/session';
12
13
 
13
14
  import { useStyles } from '../style';
14
15
  import Hero from './Hero';
@@ -22,9 +23,10 @@ const Banner = memo<{ mobile?: boolean }>(() => {
22
23
 
23
24
  const { styles } = useStyles();
24
25
 
26
+ const switchSession = useSessionStore((s) => s.switchSession);
25
27
  const handleImport = useCallback((e: any) => {
26
28
  importConfig(e);
27
- Router.push('/chat');
29
+ switchSession();
28
30
  }, []);
29
31
 
30
32
  return (
@@ -37,7 +39,13 @@ const Banner = memo<{ mobile?: boolean }>(() => {
37
39
  <Upload maxCount={1} onChange={handleImport} showUploadList={false}>
38
40
  <Button size={'large'}>{t('button.import')}</Button>
39
41
  </Upload>
40
- <Link href={'/chat'}>
42
+ <Link
43
+ href={SESSION_CHAT_URL()}
44
+ onClick={(e) => {
45
+ e.preventDefault();
46
+ switchSession();
47
+ }}
48
+ >
41
49
  <Button size={'large'} type={'primary'}>
42
50
  <Flexbox align={'center'} gap={4} horizontal>
43
51
  {t('button.start')}
@@ -1,11 +1,10 @@
1
1
  import { merge } from 'lodash-es';
2
- import { ChatCompletionFunctions } from 'openai-edge/types/api';
3
2
 
4
3
  import { LOBE_CHAT_ACCESS_CODE, OPENAI_API_KEY_HEADER_KEY, OPENAI_END_POINT } from '@/const/fetch';
5
4
  import { useGlobalStore } from '@/store/global';
6
5
  import { pluginSelectors, usePluginStore } from '@/store/plugin';
7
6
  import { initialLobeAgentConfig } from '@/store/session/initialState';
8
- import type { OpenAIStreamPayload } from '@/types/openai';
7
+ import type { ChatCompletionFunctions, OpenAIStreamPayload } from '@/types/openai';
9
8
 
10
9
  import { URLS } from './url';
11
10
 
@@ -0,0 +1,95 @@
1
+ import { isEmpty } from 'lodash-es';
2
+ import { PersistStorage } from 'zustand/middleware';
3
+ import { StorageValue } from 'zustand/middleware/persist';
4
+
5
+ import { createKeyMapper } from './keyMapper';
6
+ import { HyperStorageOptions } from './type';
7
+ import { createUrlSearch } from './urlSearch';
8
+
9
+ export const createHyperStorage = <T extends object>(
10
+ options: HyperStorageOptions,
11
+ ): PersistStorage<T> => {
12
+ const hasUrl = !!options.url;
13
+
14
+ const { setUrlSearch, getUrlSearch } = createUrlSearch(options.url?.mode);
15
+
16
+ const { mapStateKeyToStorageKey, getStateKeyFromStorageKey } = createKeyMapper(options);
17
+
18
+ return {
19
+ getItem: (name): StorageValue<T> => {
20
+ const state: any = {};
21
+ let version: number | undefined;
22
+ // ============== 处理 Local Storage ============== //
23
+ const string = localStorage.getItem(name);
24
+ if (string) {
25
+ const localState: StorageValue<T> = JSON.parse(string);
26
+ version = localState.version;
27
+ for (const [k, v] of Object.entries(localState.state)) {
28
+ const key = getStateKeyFromStorageKey(k, 'localStorage');
29
+ if (key) state[key] = v;
30
+ }
31
+ }
32
+
33
+ // ============== 处理 URL Storage ============== //
34
+ // 不从 URL 中获取 version,由于 url 状态是临时态 不作为版本控制的依据
35
+ if (hasUrl) {
36
+ const searchParameters = new URLSearchParams(getUrlSearch());
37
+
38
+ for (const [k, v] of searchParameters.entries()) {
39
+ const key = getStateKeyFromStorageKey(k, 'url');
40
+ // 当存在 UrlSelector 逻辑,且 key 有效时,才将状态加入终态 state
41
+ if (hasUrl && key) {
42
+ state[key] = v;
43
+ }
44
+ }
45
+ }
46
+
47
+ return { state, version };
48
+ },
49
+ removeItem: (key): void => {
50
+ // ============== 处理 URL Storage ============== //
51
+ if (hasUrl) {
52
+ const searchParameters = new URLSearchParams(getUrlSearch());
53
+
54
+ const storageKey = getStateKeyFromStorageKey(key, 'url');
55
+ if (storageKey) searchParameters.delete(storageKey);
56
+
57
+ setUrlSearch(searchParameters);
58
+ }
59
+ },
60
+ setItem: (name, newValue) => {
61
+ // ============== 处理 Local Storage ============== //
62
+ const localState: Record<string, any> = {};
63
+ for (const [k, v] of Object.entries(newValue.state)) {
64
+ const localKey = mapStateKeyToStorageKey(k, 'localStorage');
65
+ if (localKey) localState[localKey] = v;
66
+ }
67
+
68
+ localStorage.setItem(name, JSON.stringify({ ...newValue, state: localState }));
69
+
70
+ // ============== 处理 URL Storage ============== //
71
+ if (hasUrl) {
72
+ const searchParameters = new URLSearchParams(getUrlSearch());
73
+
74
+ for (const [k, v] of Object.entries(newValue.state)) {
75
+ const urlKey = mapStateKeyToStorageKey(k, 'url');
76
+
77
+ if (!urlKey) continue;
78
+
79
+ if (isEmpty(v)) {
80
+ searchParameters.delete(urlKey);
81
+ continue;
82
+ }
83
+
84
+ if (typeof v === 'string') {
85
+ searchParameters.set(urlKey, v);
86
+ } else {
87
+ searchParameters.set(urlKey, JSON.stringify(v));
88
+ }
89
+ }
90
+
91
+ setUrlSearch(searchParameters);
92
+ }
93
+ },
94
+ };
95
+ };
@@ -0,0 +1,53 @@
1
+ import { HyperStorageOptions } from './type';
2
+
3
+ export const createKeyMapper = (options: HyperStorageOptions) => {
4
+ const mapStateKeyToStorageKey = (
5
+ key: string,
6
+ mode: keyof HyperStorageOptions = 'localStorage',
7
+ ) => {
8
+ const selectors = options[mode]?.selectors;
9
+ if (!selectors) return key;
10
+
11
+ let storageKey: string | undefined;
12
+
13
+ if (!selectors) return key;
14
+
15
+ for (const selector of selectors) {
16
+ if (typeof selector === 'string') {
17
+ if (selector === key) storageKey = key;
18
+ } else {
19
+ if (selector[key]) storageKey = selector[key];
20
+ }
21
+ }
22
+
23
+ return storageKey;
24
+ };
25
+
26
+ const getStateKeyFromStorageKey = (
27
+ key: string,
28
+ mode: keyof HyperStorageOptions = 'localStorage',
29
+ ) => {
30
+ const selectors = options[mode]?.selectors;
31
+ if (!selectors) return key;
32
+
33
+ let stateKey: string | undefined;
34
+
35
+ for (const item of selectors) {
36
+ // 对象如果是 字符串,直接返回该 item key
37
+ if (typeof item === 'string') {
38
+ if (item === key) stateKey = key;
39
+ } else {
40
+ for (const [k, v] of Object.entries(item)) {
41
+ if (v === key) stateKey = k;
42
+ }
43
+ }
44
+ }
45
+
46
+ return stateKey;
47
+ };
48
+
49
+ return {
50
+ getStateKeyFromStorageKey,
51
+ mapStateKeyToStorageKey,
52
+ };
53
+ };
@@ -0,0 +1,14 @@
1
+ export type StorageSelector = string | Record<string, string>;
2
+
3
+ export interface HyperStorageOptions {
4
+ localStorage?: {
5
+ selectors: StorageSelector[];
6
+ };
7
+ url?: {
8
+ /**
9
+ * @default 'hash'
10
+ */
11
+ mode?: 'hash' | 'search';
12
+ selectors: StorageSelector[];
13
+ };
14
+ }
@@ -0,0 +1,17 @@
1
+ interface UrlSearchHelper {
2
+ getUrlSearch: () => string;
3
+ setUrlSearch: (params: URLSearchParams) => void;
4
+ }
5
+
6
+ export const createUrlSearch = (mode: 'search' | 'hash' = 'hash'): UrlSearchHelper => {
7
+ if (mode === 'hash')
8
+ return {
9
+ getUrlSearch: () => location.hash.slice(1),
10
+ setUrlSearch: (params: URLSearchParams) => (location.hash = params.toString()),
11
+ };
12
+
13
+ return {
14
+ getUrlSearch: () => location.search.slice(1),
15
+ setUrlSearch: (params: URLSearchParams) => (location.search = params.toString()),
16
+ };
17
+ };
@@ -1,8 +1,8 @@
1
1
  import { uniqBy } from 'lodash-es';
2
- import { ChatCompletionFunctions } from 'openai-edge/types/api';
3
2
 
4
3
  import { PLUGIN_SCHEMA_SEPARATOR } from '@/const/plugin';
5
4
  import { pluginHelpers } from '@/store/plugin/helpers';
5
+ import { ChatCompletionFunctions } from '@/types/openai';
6
6
 
7
7
  import { PluginStoreState } from './initialState';
8
8
 
@@ -4,6 +4,7 @@ import Router from 'next/router';
4
4
  import { StateCreator } from 'zustand/vanilla';
5
5
 
6
6
  import { INBOX_SESSION_ID } from '@/const/session';
7
+ import { SESSION_CHAT_URL } from '@/const/url';
7
8
  import { useGlobalStore } from '@/store/global';
8
9
  import { SessionStore } from '@/store/session';
9
10
  import { LobeAgentSession, LobeSessions } from '@/types/session';
@@ -54,12 +55,14 @@ export interface SessionAction {
54
55
  */
55
56
  removeSession: (sessionId: string) => void;
56
57
 
58
+ switchBackToChat: () => void;
59
+
57
60
  /**
58
61
  * @title 切换会话
59
62
  * @param sessionId - 会话索引
60
63
  * @returns void
61
64
  */
62
- switchSession: (sessionId?: string) => Promise<void>;
65
+ switchSession: (sessionId?: string) => void;
63
66
  }
64
67
 
65
68
  export const createSessionSlice: StateCreator<
@@ -69,7 +72,7 @@ export const createSessionSlice: StateCreator<
69
72
  SessionAction
70
73
  > = (set, get) => ({
71
74
  activeSession: (sessionId) => {
72
- set({ activeId: sessionId }, false, t('activeSession'));
75
+ set({ activeId: sessionId, activeTopicId: undefined }, false, t('activeSession'));
73
76
  },
74
77
 
75
78
  clearSessions: () => {
@@ -144,18 +147,24 @@ export const createSessionSlice: StateCreator<
144
147
  get().dispatchSession({ id: sessionId, type: 'removeSession' });
145
148
 
146
149
  if (sessionId === get().activeId) {
147
- Router.push('/chat');
150
+ get().switchSession();
148
151
  }
149
152
  },
150
153
 
151
- switchSession: async (sessionId) => {
154
+ switchBackToChat: () => {
155
+ const { activeId } = get();
156
+
157
+ const id = activeId || INBOX_SESSION_ID;
158
+
159
+ get().activeSession(id);
160
+
161
+ Router.push(SESSION_CHAT_URL(id));
162
+ },
163
+ switchSession: (sessionId = INBOX_SESSION_ID) => {
152
164
  if (get().activeId === sessionId) return;
153
165
 
154
- if (sessionId) {
155
- get().activeSession(sessionId);
156
- }
166
+ get().activeSession(sessionId);
157
167
 
158
- // 新会话
159
- await Router.push(`/chat/${sessionId}`);
168
+ Router.push(SESSION_CHAT_URL(sessionId));
160
169
  },
161
170
  });
@@ -9,9 +9,8 @@ export interface SessionState {
9
9
  /**
10
10
  * @title 当前活动的会话
11
11
  * @description 当前正在编辑或查看的会话
12
- * @default null
13
12
  */
14
- activeId: string | null;
13
+ activeId: string | undefined;
15
14
  // 默认会话
16
15
  inbox: LobeAgentSession;
17
16
  searchKeywords: string;
@@ -39,7 +38,7 @@ export const initInbox = merge({}, initLobeSession, {
39
38
  } as Partial<LobeAgentSession>);
40
39
 
41
40
  export const initialSessionState: SessionState = {
42
- activeId: null,
41
+ activeId: undefined,
43
42
  inbox: initInbox,
44
43
  searchKeywords: '',
45
44
  sessions: {},
@@ -1,3 +1,6 @@
1
+ import { INBOX_SESSION_ID } from '@/const/session';
2
+ import { SessionStore } from '@/store/session';
3
+
1
4
  import { exportAgents, exportSessions, getExportAgent } from './export';
2
5
  import {
3
6
  currentSession,
@@ -8,6 +11,8 @@ import {
8
11
  sessionList,
9
12
  } from './list';
10
13
 
14
+ const isInboxSession = (s: SessionStore) => s.activeId === INBOX_SESSION_ID;
15
+
11
16
  export const sessionSelectors = {
12
17
  currentSession,
13
18
  currentSessionSafe,
@@ -17,5 +22,6 @@ export const sessionSelectors = {
17
22
  getSessionById,
18
23
  getSessionMetaById,
19
24
  hasSessionList,
25
+ isInboxSession,
20
26
  sessionList,
21
27
  };
@@ -5,6 +5,7 @@ import { StateCreator } from 'zustand/vanilla';
5
5
 
6
6
  import { isDev } from '@/utils/env';
7
7
 
8
+ import { createHyperStorage } from '../middleware/createHyperStorage';
8
9
  import { SessionStoreState, initialState } from './initialState';
9
10
  import { AgentAction, createAgentSlice } from './slices/agentConfig/action';
10
11
  import { ChatAction, createChatSlice } from './slices/chat/actions';
@@ -22,20 +23,27 @@ const createStore: StateCreator<SessionStore, [['zustand/devtools', never]]> = (
22
23
 
23
24
  // =============== persist 本地缓存中间件配置 ============ //
24
25
 
25
- type SessionPersist = Pick<SessionStore, 'sessions' | 'inbox'>;
26
-
27
26
  const LOBE_CHAT = 'LOBE_CHAT';
28
27
 
29
- const persistOptions: PersistOptions<SessionStore, SessionPersist> = {
28
+ const persistOptions: PersistOptions<SessionStore> = {
30
29
  name: LOBE_CHAT,
31
30
 
32
- partialize: (s) => ({
33
- inbox: s.inbox,
34
- sessions: s.sessions,
35
- }),
36
-
37
31
  // 手动控制 Hydration ,避免 ssr 报错
38
32
  skipHydration: true,
33
+
34
+ storage: createHyperStorage({
35
+ localStorage: {
36
+ selectors: ['inbox', 'sessions'],
37
+ },
38
+ url: {
39
+ mode: 'hash',
40
+ selectors: [
41
+ // map state key to storage key
42
+ { activeId: 'session' },
43
+ { activeTopicId: 'topic' },
44
+ ],
45
+ },
46
+ }),
39
47
  version: 0,
40
48
  };
41
49
 
@@ -68,3 +68,26 @@ export interface OpenAIStreamPayload {
68
68
  */
69
69
  top_p?: number;
70
70
  }
71
+
72
+ export interface ChatCompletionFunctions {
73
+ /**
74
+ * The description of what the function does.
75
+ * @type {string}
76
+ * @memberof ChatCompletionFunctions
77
+ */
78
+ description?: string;
79
+ /**
80
+ * The name of the function to be called. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with a maximum length of 64.
81
+ * @type {string}
82
+ * @memberof ChatCompletionFunctions
83
+ */
84
+ name: string;
85
+ /**
86
+ * The parameters the functions accepts, described as a JSON Schema object. See the [guide](/docs/guides/gpt/function-calling) for examples, and the [JSON Schema reference](https://json-schema.org/understanding-json-schema/) for documentation about the format.
87
+ * @type {{ [key: string]: any }}
88
+ * @memberof ChatCompletionFunctions
89
+ */
90
+ parameters?: {
91
+ [key: string]: any;
92
+ };
93
+ }
@@ -1,34 +0,0 @@
1
- import { useRouter } from 'next/router';
2
- import { PropsWithChildren, memo } from 'react';
3
- import { shallow } from 'zustand/shallow';
4
-
5
- import { usePluginStore } from '@/store/plugin';
6
- import { useOnFinishHydrationSession, useSessionStore } from '@/store/session';
7
-
8
- import ChatLayout from '../layout';
9
-
10
- const Layout = memo<PropsWithChildren>(({ children }) => {
11
- const useFetchPluginList = usePluginStore((s) => s.useFetchPluginList);
12
- const [activeSession, toggleTopic] = useSessionStore((s) => {
13
- return [s.activeSession, s.toggleTopic];
14
- }, shallow);
15
-
16
- const router = useRouter();
17
- const { id } = router.query;
18
-
19
- useOnFinishHydrationSession(() => {
20
- // 1. 正常激活会话
21
- if (typeof id === 'string') {
22
- activeSession(id);
23
- }
24
-
25
- // 将话题重置为默认值
26
- toggleTopic();
27
- }, [id]);
28
-
29
- useFetchPluginList();
30
-
31
- return <ChatLayout>{children}</ChatLayout>;
32
- });
33
-
34
- export default Layout;
@@ -1,31 +0,0 @@
1
- import { useRouter } from 'next/router';
2
- import { PropsWithChildren, memo } from 'react';
3
- import { shallow } from 'zustand/shallow';
4
-
5
- import AppMobileLayout from '@/layout/AppMobileLayout';
6
- import { useOnFinishHydrationSession, useSessionStore } from '@/store/session';
7
-
8
- import Header from '../features/Header';
9
-
10
- const MobileLayout = memo<PropsWithChildren>(({ children }) => {
11
- const [activeSession, toggleTopic] = useSessionStore((s) => {
12
- return [s.activeSession, s.toggleTopic];
13
- }, shallow);
14
-
15
- const router = useRouter();
16
- const { id } = router.query;
17
-
18
- useOnFinishHydrationSession(() => {
19
- // 1. 正常激活会话
20
- if (typeof id === 'string') {
21
- activeSession(id);
22
- }
23
-
24
- // 将话题重置为默认值
25
- toggleTopic();
26
- }, [id]);
27
-
28
- return <AppMobileLayout navBar={<Header />}>{children}</AppMobileLayout>;
29
- });
30
-
31
- export default MobileLayout;
@@ -1,49 +0,0 @@
1
- import { isEmpty } from 'lodash-es';
2
- import { PersistStorage } from 'zustand/middleware';
3
-
4
- import { Compressor } from '@/utils/compass';
5
-
6
- export const createHashStorage = <T extends object>(): PersistStorage<T> => {
7
- return {
8
- getItem: async () => {
9
- const searchParameters = new URLSearchParams(location.hash.slice(1));
10
- const state: any = {};
11
- const pool = [...searchParameters.entries()].map(async ([k, v]) => {
12
- const string_ = await Compressor.decompressAsync(v);
13
-
14
- try {
15
- state[k] = JSON.parse(string_);
16
- } catch {
17
- state[k] = string_;
18
- }
19
- });
20
-
21
- await Promise.all(pool);
22
-
23
- return { state } as any;
24
- },
25
- removeItem: (key): void => {
26
- const searchParameters = new URLSearchParams(location.hash.slice(1));
27
- searchParameters.delete(key);
28
- location.hash = searchParameters.toString();
29
- },
30
- setItem: async (_, newValue) => {
31
- const searchParameters = new URLSearchParams(location.hash.slice(1));
32
-
33
- const pool = Object.entries(newValue.state).map(async ([k, v]) => {
34
- if (isEmpty(v)) {
35
- searchParameters.delete(k);
36
- return;
37
- }
38
- if (typeof v === 'string') {
39
- searchParameters.set(k, await Compressor.compressAsync(v));
40
- } else {
41
- searchParameters.set(k, await Compressor.compressAsync(JSON.stringify(v)));
42
- }
43
- });
44
- await Promise.all(pool);
45
-
46
- location.hash = searchParameters.toString();
47
- },
48
- };
49
- };