@lobehub/chat 0.69.1 → 0.70.1

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 (45) hide show
  1. package/.github/workflows/contributor-help.yml +2 -4
  2. package/CHANGELOG.md +50 -0
  3. package/package.json +1 -1
  4. package/src/const/url.ts +3 -0
  5. package/src/features/MobileTabBar/index.tsx +3 -1
  6. package/src/features/SideBar/TopActions.tsx +4 -3
  7. package/src/helpers/export.ts +2 -2
  8. package/src/pages/chat/features/Header/Mobile.tsx +6 -8
  9. package/src/pages/chat/features/Header/PC.tsx +15 -16
  10. package/src/pages/chat/features/Header/index.tsx +3 -3
  11. package/src/pages/chat/{SessionList → features/SessionList}/Inbox.tsx +9 -2
  12. package/src/pages/chat/{SessionList → features/SessionList}/List/Item/index.tsx +2 -2
  13. package/src/pages/chat/{SessionList → features/SessionList}/List/index.tsx +10 -1
  14. package/src/pages/chat/features/Sidebar/index.tsx +6 -2
  15. package/src/pages/chat/index.page.tsx +9 -11
  16. package/src/pages/chat/layout.tsx +6 -1
  17. package/src/pages/chat/{[id] → mobile}/index.page.tsx +7 -17
  18. package/src/pages/chat/mobile.tsx +2 -2
  19. package/src/pages/chat/{[id]/setting → setting}/Header.tsx +3 -4
  20. package/src/pages/index.page.tsx +10 -4
  21. package/src/pages/settings/Header.tsx +4 -2
  22. package/src/pages/settings/Settings/Agent.tsx +3 -3
  23. package/src/pages/settings/Settings/Common.tsx +2 -2
  24. package/src/pages/welcome/Banner/index.tsx +11 -3
  25. package/src/store/global/selectors.ts +9 -11
  26. package/src/store/middleware/createHyperStorage/index.ts +95 -0
  27. package/src/store/middleware/createHyperStorage/keyMapper.ts +53 -0
  28. package/src/store/middleware/createHyperStorage/type.ts +14 -0
  29. package/src/store/middleware/createHyperStorage/urlSearch.ts +17 -0
  30. package/src/store/session/slices/chat/selectors/token.ts +8 -9
  31. package/src/store/session/slices/session/action.ts +18 -9
  32. package/src/store/session/slices/session/initialState.ts +2 -3
  33. package/src/store/session/slices/session/selectors/index.ts +6 -0
  34. package/src/store/session/store.ts +16 -8
  35. package/src/pages/chat/[id]/layout.tsx +0 -34
  36. package/src/pages/chat/[id]/mobile.tsx +0 -31
  37. package/src/store/middleware/createHashStorage.ts +0 -49
  38. /package/src/pages/chat/{SessionList → features/SessionList}/CollapseGroup.tsx +0 -0
  39. /package/src/pages/chat/{SessionList → features/SessionList}/Header.tsx +0 -0
  40. /package/src/pages/chat/{SessionList → features/SessionList}/List/AddButton.tsx +0 -0
  41. /package/src/pages/chat/{SessionList → features/SessionList}/List/Item/Actions.tsx +0 -0
  42. /package/src/pages/chat/{SessionList → features/SessionList}/List/SkeletonList.tsx +0 -0
  43. /package/src/pages/chat/{SessionList → features/SessionList}/index.tsx +0 -0
  44. /package/src/pages/chat/{[id]/setting → setting}/index.page.tsx +0 -0
  45. /package/src/pages/chat/{[id]/setting → setting}/mobile.tsx +0 -0
@@ -1,9 +1,7 @@
1
1
  name: Contributor Helper
2
2
 
3
3
  on:
4
- # 🌏 Think about the planet! No need to update stats too frequently
5
4
  schedule: [{ cron: '0 18 * * *' }]
6
- # 💡 The following line lets you run workflow manually from the action tab!
7
5
  workflow_dispatch:
8
6
  jobs:
9
7
  contributor:
@@ -21,8 +19,8 @@ jobs:
21
19
  - name: Commit and push if changed
22
20
  run: |-
23
21
  git diff
24
- git config --global user.email "actions@github.com"
25
- git config --global user.name "github-actions"
22
+ git config --global user.name "lobehubbot"
23
+ git config --global user.email "i@lobehub.com"
26
24
  git pull
27
25
  git add -A
28
26
  git commit -m "🤖 docs: Auto update contributors" || exit 0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,56 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.70.1](https://github.com/lobehub/lobe-chat/compare/v0.70.0...v0.70.1)
6
+
7
+ <sup>Released on **2023-09-08**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Refactor settingsSelectors to globalSelectors.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **misc**: Refactor settingsSelectors to globalSelectors ([38917e8](https://github.com/lobehub/lobe-chat/commit/38917e8))
21
+
22
+ </details>
23
+
24
+ <div align="right">
25
+
26
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
27
+
28
+ </div>
29
+
30
+ ## [Version 0.70.0](https://github.com/lobehub/lobe-chat/compare/v0.69.1...v0.70.0)
31
+
32
+ <sup>Released on **2023-09-08**</sup>
33
+
34
+ #### ✨ Features
35
+
36
+ - **misc**: Refactor to url state.
37
+
38
+ <br/>
39
+
40
+ <details>
41
+ <summary><kbd>Improvements and Fixes</kbd></summary>
42
+
43
+ #### What's improved
44
+
45
+ - **misc**: Refactor to url state, closes [#157](https://github.com/lobehub/lobe-chat/issues/157) ([2efac2b](https://github.com/lobehub/lobe-chat/commit/2efac2b))
46
+
47
+ </details>
48
+
49
+ <div align="right">
50
+
51
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
52
+
53
+ </div>
54
+
5
55
  ### [Version 0.69.1](https://github.com/lobehub/lobe-chat/compare/v0.69.0...v0.69.1)
6
56
 
7
57
  <sup>Released on **2023-09-06**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.69.1",
3
+ "version": "0.70.1",
4
4
  "description": "Lobe Chat is an open-source chatbot client using LangChain, Typescript and Next.js",
5
5
  "keywords": [
6
6
  "chatbot",
package/src/const/url.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import pkg from '../../package.json';
2
+ import { INBOX_SESSION_ID } from './session';
2
3
 
3
4
  export const GITHUB = pkg.homepage;
4
5
  export const CHANGELOG = `${pkg.homepage}/blob/master/CHANGELOG.md`;
@@ -8,3 +9,5 @@ export const DISCORD = 'https://discord.gg/AYFPHvv2jT';
8
9
 
9
10
  export const PLUGINS_INDEX_URL =
10
11
  process.env.PLUGINS_INDEX_URL ?? 'https://chat-plugins.lobehub.com';
12
+
13
+ export const SESSION_CHAT_URL = (id: string = INBOX_SESSION_ID) => `/chat#session=${id}`;
@@ -7,6 +7,7 @@ import { memo, useMemo } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
 
9
9
  import { useGlobalStore } from '@/store/global';
10
+ import { useSessionStore } from '@/store/session';
10
11
 
11
12
  const useStyles = createStyles(({ css, token }) => ({
12
13
  active: css`
@@ -20,6 +21,7 @@ export default memo<{ className?: string }>(({ className }) => {
20
21
  const [tab, setTab] = useGlobalStore((s) => [s.sidebarKey, s.switchSideBar]);
21
22
  const { t } = useTranslation('common');
22
23
  const { styles } = useStyles();
24
+ const switchSession = useSessionStore((s) => s.switchSession);
23
25
  const items: MobileTabBarProps['items'] = useMemo(
24
26
  () => [
25
27
  {
@@ -28,7 +30,7 @@ export default memo<{ className?: string }>(({ className }) => {
28
30
  ),
29
31
  key: 'chat',
30
32
  onClick: () => {
31
- Router.push('/chat');
33
+ switchSession();
32
34
  },
33
35
  title: t('tab.chat'),
34
36
  },
@@ -5,6 +5,7 @@ import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { GlobalStore } from '@/store/global';
8
+ import { useSessionStore } from '@/store/session';
8
9
 
9
10
  export interface TopActionProps {
10
11
  setTab: GlobalStore['switchSideBar'];
@@ -13,7 +14,7 @@ export interface TopActionProps {
13
14
 
14
15
  const TopActions = memo<TopActionProps>(({ tab, setTab }) => {
15
16
  const { t } = useTranslation('common');
16
-
17
+ const switchBackToChat = useSessionStore((s) => s.switchBackToChat);
17
18
  return (
18
19
  <>
19
20
  <ActionIcon
@@ -22,7 +23,7 @@ const TopActions = memo<TopActionProps>(({ tab, setTab }) => {
22
23
  onClick={() => {
23
24
  // 如果已经在 chat 路径下了,那么就不用再跳转了
24
25
  if (Router.asPath.startsWith('/chat')) return;
25
- Router.push('/chat');
26
+ switchBackToChat();
26
27
  setTab('chat');
27
28
  }}
28
29
  size="large"
@@ -31,7 +32,7 @@ const TopActions = memo<TopActionProps>(({ tab, setTab }) => {
31
32
  <ActionIcon
32
33
  active={tab === 'market'}
33
34
  icon={Sticker}
34
- onClick={() => setTab('market')}
35
+ // onClick={() => setTab('market')}
35
36
  size="large"
36
37
  title={t('tab.market')}
37
38
  />
@@ -1,4 +1,4 @@
1
- import { settingsSelectors, useGlobalStore } from '@/store/global';
1
+ import { globalSelectors, useGlobalStore } from '@/store/global';
2
2
  import { useSessionStore } from '@/store/session';
3
3
  import { sessionSelectors } from '@/store/session/selectors';
4
4
  import { createConfigFile, exportConfigFile } from '@/utils/config';
@@ -6,7 +6,7 @@ import { createConfigFile, exportConfigFile } from '@/utils/config';
6
6
  const getSessions = () => sessionSelectors.exportSessions(useSessionStore.getState());
7
7
  const getSession = (id: string) => sessionSelectors.getSessionById(id)(useSessionStore.getState());
8
8
 
9
- const getSettings = () => settingsSelectors.exportSettings(useGlobalStore.getState());
9
+ const getSettings = () => globalSelectors.exportSettings(useGlobalStore.getState());
10
10
 
11
11
  // ============= 导出所有角色 ============= //
12
12
 
@@ -4,21 +4,19 @@ import Router from 'next/router';
4
4
  import { memo } from 'react';
5
5
  import { useTranslation } from 'react-i18next';
6
6
 
7
- import { INBOX_SESSION_ID } from '@/const/session';
8
7
  import { useGlobalStore } from '@/store/global';
9
8
  import { useSessionStore } from '@/store/session';
10
- import { agentSelectors } from '@/store/session/selectors';
9
+ import { agentSelectors, sessionSelectors } from '@/store/session/selectors';
11
10
 
12
- const MobileHeader = memo<{ settings?: boolean }>(({ settings = true }) => {
11
+ const MobileHeader = memo(() => {
13
12
  const { t } = useTranslation('common');
14
13
 
15
- const [id, title, model] = useSessionStore((s) => [
16
- s.activeId,
14
+ const [isInbox, title, model] = useSessionStore((s) => [
15
+ sessionSelectors.isInboxSession(s),
17
16
  agentSelectors.currentAgentTitle(s),
18
17
  agentSelectors.currentAgentModel(s),
19
18
  ]);
20
19
 
21
- const isInbox = id === INBOX_SESSION_ID;
22
20
  const [toggleConfig] = useGlobalStore((s) => [s.toggleMobileTopic]);
23
21
 
24
22
  const displayTitle = isInbox ? t('inbox.title') : title;
@@ -30,11 +28,11 @@ const MobileHeader = memo<{ settings?: boolean }>(({ settings = true }) => {
30
28
  right={
31
29
  <>
32
30
  <ActionIcon icon={LayoutList} onClick={() => toggleConfig()} />
33
- {settings && (
31
+ {!isInbox && (
34
32
  <ActionIcon
35
33
  icon={Settings}
36
34
  onClick={() => {
37
- Router.push(`/chat/${id}/setting`);
35
+ Router.push({ hash: location.hash, pathname: `/chat/setting` });
38
36
  }}
39
37
  />
40
38
  )}
@@ -7,31 +7,30 @@ import { memo } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
 
10
- import { INBOX_SESSION_ID } from '@/const/session';
11
10
  import { useGlobalStore } from '@/store/global';
12
11
  import { useSessionChatInit, useSessionStore } from '@/store/session';
13
- import { agentSelectors } from '@/store/session/selectors';
12
+ import { agentSelectors, sessionSelectors } from '@/store/session/selectors';
14
13
 
15
14
  import PluginTag from './PluginTag';
16
15
  import ShareButton from './ShareButton';
17
16
 
18
- const Header = memo<{ settings?: boolean }>(({ settings = true }) => {
17
+ const Header = memo(() => {
19
18
  const init = useSessionChatInit();
20
19
 
21
20
  const { t } = useTranslation('common');
22
21
 
23
- const [id, title, description, avatar, backgroundColor, model, plugins] = useSessionStore((s) => [
24
- s.activeId,
25
-
26
- agentSelectors.currentAgentTitle(s),
27
- agentSelectors.currentAgentDescription(s),
28
- agentSelectors.currentAgentAvatar(s),
29
- agentSelectors.currentAgentBackgroundColor(s),
30
- agentSelectors.currentAgentModel(s),
31
- agentSelectors.currentAgentPlugins(s),
32
- ]);
22
+ const [isInbox, title, description, avatar, backgroundColor, model, plugins] = useSessionStore(
23
+ (s) => [
24
+ sessionSelectors.isInboxSession(s),
25
+ agentSelectors.currentAgentTitle(s),
26
+ agentSelectors.currentAgentDescription(s),
27
+ agentSelectors.currentAgentAvatar(s),
28
+ agentSelectors.currentAgentBackgroundColor(s),
29
+ agentSelectors.currentAgentModel(s),
30
+ agentSelectors.currentAgentPlugins(s),
31
+ ],
32
+ );
33
33
 
34
- const isInbox = id === INBOX_SESSION_ID;
35
34
  const [showAgentSettings, toggleConfig] = useGlobalStore((s) => [
36
35
  s.preference.showChatSideBar,
37
36
  s.toggleChatSideBar,
@@ -77,11 +76,11 @@ const Header = memo<{ settings?: boolean }>(({ settings = true }) => {
77
76
  size={{ fontSize: 24 }}
78
77
  title={t('roleAndArchive')}
79
78
  />
80
- {settings && (
79
+ {!isInbox && (
81
80
  <ActionIcon
82
81
  icon={Settings}
83
82
  onClick={() => {
84
- Router.push(`/chat/${id}/setting`);
83
+ Router.push({ hash: location.hash, pathname: `/chat/setting` });
85
84
  }}
86
85
  size={{ fontSize: 24 }}
87
86
  title={t('header.session', { ns: 'setting' })}
@@ -4,12 +4,12 @@ import { memo } from 'react';
4
4
  import Mobile from './Mobile';
5
5
  import PC from './PC';
6
6
 
7
- const Header = memo<{ settings?: boolean }>((props) => {
7
+ const Header = memo(() => {
8
8
  const { mobile } = useResponsive();
9
9
 
10
- if (mobile) return <Mobile {...props} />;
10
+ if (mobile) return <Mobile />;
11
11
 
12
- return <PC {...props} />;
12
+ return <PC />;
13
13
  });
14
14
 
15
15
  export default Header;
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
6
6
 
7
7
  import { DEFAULT_INBOX_AVATAR } from '@/const/meta';
8
8
  import { INBOX_SESSION_ID } from '@/const/session';
9
+ import { SESSION_CHAT_URL } from '@/const/url';
9
10
  import { useSessionStore } from '@/store/session';
10
11
 
11
12
  const { Item } = List;
@@ -14,7 +15,7 @@ const Inbox = memo(() => {
14
15
  const ref = useRef(null);
15
16
  const isHovering = useHover(ref);
16
17
  const { t } = useTranslation('common');
17
- const [activeId] = useSessionStore((s) => [s.activeId]);
18
+ const [activeId, switchSession] = useSessionStore((s) => [s.activeId, s.switchSession]);
18
19
 
19
20
  const avatarRender = useMemo(
20
21
  () => (
@@ -29,7 +30,13 @@ const Inbox = memo(() => {
29
30
  );
30
31
 
31
32
  return (
32
- <Link href={'/chat'}>
33
+ <Link
34
+ href={SESSION_CHAT_URL()}
35
+ onClick={(e) => {
36
+ e.preventDefault();
37
+ switchSession(INBOX_SESSION_ID);
38
+ }}
39
+ >
33
40
  <Item
34
41
  active={activeId === INBOX_SESSION_ID}
35
42
  avatar={avatarRender}
@@ -6,7 +6,7 @@ import { memo, useMemo, useRef, useState } from 'react';
6
6
  import { Flexbox } from 'react-layout-kit';
7
7
  import { shallow } from 'zustand/shallow';
8
8
 
9
- import { settingsSelectors, useGlobalStore } from '@/store/global';
9
+ import { globalSelectors, useGlobalStore } from '@/store/global';
10
10
  import { useSessionStore } from '@/store/session';
11
11
  import { agentSelectors, sessionSelectors } from '@/store/session/selectors';
12
12
 
@@ -36,7 +36,7 @@ const SessionItem = memo<SessionItemProps>(({ id }) => {
36
36
  const isHovering = useHover(ref);
37
37
 
38
38
  const { styles } = useStyles();
39
- const [defaultModel] = useGlobalStore((s) => [settingsSelectors.currentAgentConfig(s).model]);
39
+ const [defaultModel] = useGlobalStore((s) => [globalSelectors.defaultAgentConfig(s).model]);
40
40
 
41
41
  const [
42
42
  active,
@@ -2,6 +2,7 @@ import isEqual from 'fast-deep-equal';
2
2
  import Link from 'next/link';
3
3
  import { memo } from 'react';
4
4
 
5
+ import { SESSION_CHAT_URL } from '@/const/url';
5
6
  import { useSessionHydrated, useSessionStore } from '@/store/session';
6
7
  import { sessionSelectors } from '@/store/session/selectors';
7
8
 
@@ -11,6 +12,7 @@ import SkeletonList from './SkeletonList';
11
12
 
12
13
  const SessionList = memo(() => {
13
14
  const list = useSessionStore(sessionSelectors.sessionList, isEqual);
15
+ const activeSession = useSessionStore((s) => s.activeSession);
14
16
 
15
17
  const isInit = useSessionHydrated();
16
18
 
@@ -18,7 +20,14 @@ const SessionList = memo(() => {
18
20
  <SkeletonList />
19
21
  ) : list.length > 0 ? (
20
22
  list.map(({ id }) => (
21
- <Link href={`/chat/${id}`} key={id}>
23
+ <Link
24
+ href={SESSION_CHAT_URL(id)}
25
+ key={id}
26
+ onClick={(e) => {
27
+ e.preventDefault();
28
+ activeSession(id);
29
+ }}
30
+ >
22
31
  <SessionItem id={id} />
23
32
  </Link>
24
33
  ))
@@ -8,6 +8,8 @@ import { Flexbox } from 'react-layout-kit';
8
8
  import SafeSpacing from '@/components/SafeSpacing';
9
9
  import { CHAT_SIDEBAR_WIDTH } from '@/const/layoutTokens';
10
10
  import { useGlobalStore } from '@/store/global';
11
+ import { useSessionStore } from '@/store/session';
12
+ import { sessionSelectors } from '@/store/session/slices/session/selectors';
11
13
 
12
14
  import Mobile from './Mobile';
13
15
  import SystemRole from './SystemRole';
@@ -37,13 +39,15 @@ interface SideBarProps {
37
39
  mobile?: boolean;
38
40
  systemRole?: boolean;
39
41
  }
40
- const SideBar = memo<SideBarProps>(({ systemRole = true, mobile }) => {
42
+ const SideBar = memo<SideBarProps>(({ mobile }) => {
41
43
  const { styles } = useStyles(mobile);
42
44
  const [showAgentSettings, toggleConfig] = useGlobalStore((s) => [
43
45
  s.preference.showChatSideBar,
44
46
  s.toggleChatSideBar,
45
47
  ]);
46
48
 
49
+ const isInbox = useSessionStore(sessionSelectors.isInboxSession);
50
+
47
51
  const { t } = useTranslation('common');
48
52
 
49
53
  const topic = (
@@ -79,7 +83,7 @@ const SideBar = memo<SideBarProps>(({ systemRole = true, mobile }) => {
79
83
  style={{ flex: 'none', height: '100%', minWidth: CHAT_SIDEBAR_WIDTH }}
80
84
  >
81
85
  <SafeSpacing />
82
- {systemRole && <SystemRole mobile={mobile} />}
86
+ {!isInbox && <SystemRole mobile={mobile} />}
83
87
 
84
88
  {topic}
85
89
  </DraggablePanelContainer>
@@ -1,11 +1,10 @@
1
1
  import { useResponsive } from 'antd-style';
2
2
  import Head from 'next/head';
3
3
  import { memo } from 'react';
4
- import { useTranslation } from 'react-i18next';
5
4
  import { Flexbox } from 'react-layout-kit';
6
5
 
7
- import { INBOX_SESSION_ID } from '@/const/session';
8
- import { useOnFinishHydrationSession, useSessionStore } from '@/store/session';
6
+ import { useSessionStore } from '@/store/session';
7
+ import { agentSelectors } from '@/store/session/selectors';
9
8
  import { genSiteHeadTitle } from '@/utils/genSiteHeadTitle';
10
9
 
11
10
  import Conversation from './features/Conversation';
@@ -15,13 +14,12 @@ import Layout from './layout';
15
14
  import Mobile from './mobile';
16
15
 
17
16
  const Chat = memo(() => {
18
- const { t } = useTranslation('common');
19
17
  const { mobile } = useResponsive();
20
- const pageTitle = genSiteHeadTitle(t('inbox.title'));
21
-
22
- useOnFinishHydrationSession(() => {
23
- useSessionStore.getState().activeSession(INBOX_SESSION_ID);
24
- });
18
+ const [avatar, title] = useSessionStore((s) => [
19
+ agentSelectors.currentAgentAvatar(s),
20
+ agentSelectors.currentAgentTitle(s),
21
+ ]);
22
+ const pageTitle = genSiteHeadTitle([avatar, title].filter(Boolean).join(' '));
25
23
 
26
24
  return (
27
25
  <>
@@ -32,10 +30,10 @@ const Chat = memo(() => {
32
30
  <Mobile />
33
31
  ) : (
34
32
  <Layout>
35
- <Header settings={false} />
33
+ <Header />
36
34
  <Flexbox flex={1} height={'calc(100vh - 64px)'} horizontal>
37
35
  <Conversation />
38
- <SideBar systemRole={false} />
36
+ <SideBar />
39
37
  </Flexbox>
40
38
  </Layout>
41
39
  )}
@@ -4,13 +4,18 @@ import { Flexbox } from 'react-layout-kit';
4
4
 
5
5
  import AppLayout from '@/layout/AppLayout';
6
6
  import { useSwitchSideBarOnInit } from '@/store/global';
7
+ import { usePluginStore } from '@/store/plugin';
7
8
 
8
- import { Sessions } from './SessionList';
9
+ import { Sessions } from './features/SessionList';
9
10
 
10
11
  const ChatLayout = memo<PropsWithChildren>(({ children }) => {
11
12
  const { mobile } = useResponsive();
13
+
12
14
  useSwitchSideBarOnInit('chat');
13
15
 
16
+ const useFetchPluginList = usePluginStore((s) => s.useFetchPluginList);
17
+ useFetchPluginList();
18
+
14
19
  return (
15
20
  <AppLayout>
16
21
  <Sessions />
@@ -1,20 +1,18 @@
1
- import { useResponsive } from 'antd-style';
2
1
  import Head from 'next/head';
3
2
  import { memo } from 'react';
4
3
  import { Flexbox } from 'react-layout-kit';
5
4
 
5
+ import AppMobileLayout from '@/layout/AppMobileLayout';
6
+ import Header from '@/pages/chat/features/Header';
6
7
  import { useSessionStore } from '@/store/session';
7
8
  import { agentSelectors } from '@/store/session/selectors';
8
9
  import { genSiteHeadTitle } from '@/utils/genSiteHeadTitle';
9
10
 
10
11
  import Conversation from '../features/Conversation';
11
- import Header from '../features/Header';
12
12
  import SideBar from '../features/Sidebar';
13
- import Layout from './layout';
14
- import Mobile from './mobile';
13
+ import ChatLayout from '../layout';
15
14
 
16
15
  const Chat = memo(() => {
17
- const { mobile } = useResponsive();
18
16
  const [avatar, title] = useSessionStore((s) => [
19
17
  agentSelectors.currentAgentAvatar(s),
20
18
  agentSelectors.currentAgentTitle(s),
@@ -27,22 +25,14 @@ const Chat = memo(() => {
27
25
  <Head>
28
26
  <title>{pageTitle}</title>
29
27
  </Head>
30
- {mobile ? (
31
- <Mobile>
28
+ <AppMobileLayout navBar={<Header />}>
29
+ <ChatLayout>
32
30
  <Flexbox height={'calc(100vh - 44px)'} horizontal>
33
31
  <Conversation mobile />
34
32
  <SideBar mobile />
35
33
  </Flexbox>
36
- </Mobile>
37
- ) : (
38
- <Layout>
39
- <Header />
40
- <Flexbox flex={1} height={'calc(100vh - 64px)'} horizontal>
41
- <Conversation />
42
- <SideBar />
43
- </Flexbox>
44
- </Layout>
45
- )}
34
+ </ChatLayout>
35
+ </AppMobileLayout>
46
36
  </>
47
37
  );
48
38
  });
@@ -2,10 +2,10 @@ import { useTheme } from 'antd-style';
2
2
  import { memo } from 'react';
3
3
 
4
4
  import AppMobileLayout from '@/layout/AppMobileLayout';
5
- import Header from '@/pages/chat/SessionList/Header';
6
5
  import { useSwitchSideBarOnInit } from '@/store/global';
7
6
 
8
- import { Sessions } from './SessionList';
7
+ import { Sessions } from './features/SessionList';
8
+ import Header from './features/SessionList/Header';
9
9
 
10
10
  const ChatMobileLayout = memo(() => {
11
11
  useSwitchSideBarOnInit('chat');
@@ -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
  );
@@ -2,11 +2,11 @@ import isEqual from 'fast-deep-equal';
2
2
  import { memo } from 'react';
3
3
 
4
4
  import AgentSetting from '@/features/AgentSetting';
5
- import { settingsSelectors, useGlobalStore } from '@/store/global';
5
+ import { globalSelectors, useGlobalStore } from '@/store/global';
6
6
 
7
7
  const Agent = memo(() => {
8
- const config = useGlobalStore(settingsSelectors.currentAgentConfig, isEqual);
9
- const meta = useGlobalStore(settingsSelectors.currentAgentMeta, isEqual);
8
+ const config = useGlobalStore(globalSelectors.defaultAgentConfig, isEqual);
9
+ const meta = useGlobalStore(globalSelectors.defaultAgentMeta, isEqual);
10
10
  const [updateAgent] = useGlobalStore((s) => [s.updateDefaultAgent]);
11
11
 
12
12
  return (
@@ -12,7 +12,7 @@ import { FORM_STYLE } from '@/const/layoutTokens';
12
12
  import { DEFAULT_SETTINGS } from '@/const/settings';
13
13
  import AvatarWithUpload from '@/features/AvatarWithUpload';
14
14
  import { options } from '@/locales/options';
15
- import { settingsSelectors, useGlobalStore } from '@/store/global';
15
+ import { globalSelectors, useGlobalStore } from '@/store/global';
16
16
  import { useSessionStore } from '@/store/session';
17
17
  import { ConfigKeys } from '@/types/settings';
18
18
 
@@ -29,7 +29,7 @@ const Common = memo(() => {
29
29
  const [form] = AntForm.useForm();
30
30
  const clearSessions = useSessionStore((s) => s.clearSessions);
31
31
 
32
- const settings = useGlobalStore(settingsSelectors.currentSettings, isEqual);
32
+ const settings = useGlobalStore(globalSelectors.currentSettings, isEqual);
33
33
  const [setThemeMode, setSettings, resetSettings] = useGlobalStore((s) => [
34
34
  s.switchThemeMode,
35
35
  s.setSettings,
@@ -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,20 +1,18 @@
1
- import { merge } from 'lodash-es';
2
-
3
1
  import { DEFAULT_AGENT_META } from '@/const/meta';
4
2
  import { DEFAULT_AGENT, DEFAULT_AGENT_CONFIG, DEFAULT_SETTINGS } from '@/const/settings';
5
3
  import { GlobalSettings } from '@/types/settings';
4
+ import { merge } from '@/utils/merge';
6
5
 
7
6
  import { GlobalStore } from './store';
8
7
 
9
8
  const currentSettings = (s: GlobalStore) => merge({}, DEFAULT_SETTINGS, s.settings);
10
9
 
11
- const currentDefaultAgent = (s: GlobalStore) => merge({}, DEFAULT_AGENT, s.settings.defaultAgent);
10
+ const defaultAgent = (s: GlobalStore) => merge({}, DEFAULT_AGENT, s.settings.defaultAgent);
12
11
 
13
- const currentAgentConfig = (s: GlobalStore) =>
14
- merge({}, DEFAULT_AGENT_CONFIG, currentDefaultAgent(s).config);
12
+ const defaultAgentConfig = (s: GlobalStore) =>
13
+ merge({}, DEFAULT_AGENT_CONFIG, defaultAgent(s).config);
15
14
 
16
- const currentAgentMeta = (s: GlobalStore) =>
17
- merge({}, DEFAULT_AGENT_META, currentDefaultAgent(s).meta);
15
+ const defaultAgentMeta = (s: GlobalStore) => merge({}, DEFAULT_AGENT_META, defaultAgent(s).meta);
18
16
 
19
17
  export const exportSettings = (s: GlobalStore) => {
20
18
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -23,10 +21,10 @@ export const exportSettings = (s: GlobalStore) => {
23
21
  return settings as GlobalSettings;
24
22
  };
25
23
 
26
- export const settingsSelectors = {
27
- currentAgentConfig,
28
- currentAgentMeta,
29
- currentDefaultAgent,
24
+ export const globalSelectors = {
30
25
  currentSettings,
26
+ defaultAgent,
27
+ defaultAgentConfig,
28
+ defaultAgentMeta,
31
29
  exportSettings,
32
30
  };
@@ -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
+ };
@@ -5,25 +5,24 @@ import { agentSelectors } from '@/store/session/slices/agentConfig';
5
5
  import type { SessionStore } from '../../../store';
6
6
  import { currentChatsWithHistoryConfig } from './chat';
7
7
 
8
+ // TODO: We should find a smaller package to do this. gpt-tokenizer has 2M Size
9
+ const count = (text: string) => encode(text).length;
10
+
8
11
  export const systemRoleSel = (s: SessionStore): string => {
9
12
  const config = agentSelectors.currentAgentConfig(s);
10
13
 
11
14
  return config.systemRole;
12
15
  };
13
16
 
14
- const systemRoleTokens = (s: SessionStore): number[] => {
17
+ export const systemRoleTokenCount = (s: SessionStore): number => {
15
18
  const systemRole = systemRoleSel(s);
16
19
 
17
- return encode(systemRole || '');
20
+ return count(systemRole || '');
18
21
  };
19
22
 
20
- const chatsTokens = (s: SessionStore): number[] => {
23
+ export const chatsTokenCount = (s: SessionStore): number => {
21
24
  const chats = currentChatsWithHistoryConfig(s);
22
- return encode(chats.map((m) => m.content).join(''));
25
+ return count(chats.map((m) => m.content).join(''));
23
26
  };
24
27
 
25
- export const chatsTokenCount = (s: SessionStore) => chatsTokens(s).length;
26
-
27
- export const systemRoleTokenCount = (s: SessionStore) => systemRoleTokens(s).length;
28
-
29
- export const totalTokenCount = (s: SessionStore) => chatsTokens(s).length + systemRoleTokenCount(s);
28
+ export const totalTokenCount = (s: SessionStore) => chatsTokenCount(s) + systemRoleTokenCount(s);
@@ -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
 
@@ -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
- };