@lobehub/chat 0.161.5 → 0.161.7

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 CHANGED
@@ -2,6 +2,48 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ### [Version 0.161.7](https://github.com/lobehub/lobe-chat/compare/v0.161.6...v0.161.7)
6
+
7
+ <sup>Released on **2024-05-22**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Refactor to serverDB ENV.
12
+
13
+ <br/>
14
+
15
+ <details>
16
+ <summary><kbd>Improvements and Fixes</kbd></summary>
17
+
18
+ #### Code refactoring
19
+
20
+ - **misc**: Refactor to serverDB ENV, closes [#2612](https://github.com/lobehub/lobe-chat/issues/2612) ([fa1409e](https://github.com/lobehub/lobe-chat/commit/fa1409e))
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.161.6](https://github.com/lobehub/lobe-chat/compare/v0.161.5...v0.161.6)
31
+
32
+ <sup>Released on **2024-05-22**</sup>
33
+
34
+ <br/>
35
+
36
+ <details>
37
+ <summary><kbd>Improvements and Fixes</kbd></summary>
38
+
39
+ </details>
40
+
41
+ <div align="right">
42
+
43
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
44
+
45
+ </div>
46
+
5
47
  ### [Version 0.161.5](https://github.com/lobehub/lobe-chat/compare/v0.161.4...v0.161.5)
6
48
 
7
49
  <sup>Released on **2024-05-22**</sup>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/chat",
3
- "version": "0.161.5",
3
+ "version": "0.161.7",
4
4
  "description": "Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -1,9 +1,14 @@
1
+ // import TopicListContent from './features/TopicListContent';
2
+ import React, { Suspense, lazy } from 'react';
3
+
1
4
  import { isMobileDevice } from '@/utils/responsive';
2
5
 
3
6
  import Desktop from './_layout/Desktop';
4
7
  import Mobile from './_layout/Mobile';
8
+ import SkeletonList from './features/SkeletonList';
5
9
  import SystemRole from './features/SystemRole';
6
- import TopicListContent from './features/TopicListContent';
10
+
11
+ const TopicContent = lazy(() => import('./features/TopicListContent'));
7
12
 
8
13
  const Topic = () => {
9
14
  const mobile = isMobileDevice();
@@ -14,7 +19,9 @@ const Topic = () => {
14
19
  <>
15
20
  {!mobile && <SystemRole />}
16
21
  <Layout>
17
- <TopicListContent />
22
+ <Suspense fallback={<SkeletonList />}>
23
+ <TopicContent />
24
+ </Suspense>
18
25
  </Layout>
19
26
  </>
20
27
  );
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  import { Skeleton } from 'antd';
2
4
  import { createStyles } from 'antd-style';
3
5
  import { memo } from 'react';
@@ -3,20 +3,19 @@
3
3
  import { EmptyCard } from '@lobehub/ui';
4
4
  import { useThemeMode } from 'antd-style';
5
5
  import isEqual from 'fast-deep-equal';
6
- import React, { Suspense, memo, useCallback, useRef } from 'react';
6
+ import React, { memo, useCallback, useMemo, useRef } from 'react';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import { Flexbox } from 'react-layout-kit';
9
9
  import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
10
10
 
11
11
  import { imageUrl } from '@/const/url';
12
- import { isServerMode } from '@/const/version';
13
12
  import { useChatStore } from '@/store/chat';
14
13
  import { topicSelectors } from '@/store/chat/selectors';
15
14
  import { useSessionStore } from '@/store/session';
16
15
  import { useUserStore } from '@/store/user';
17
16
  import { ChatTopic } from '@/types/topic';
18
17
 
19
- import { Placeholder, SkeletonList } from './SkeletonList';
18
+ import { Placeholder, SkeletonList } from '../SkeletonList';
20
19
  import TopicItem from './TopicItem';
21
20
 
22
21
  const TopicListContent = memo(() => {
@@ -34,21 +33,19 @@ const TopicListContent = memo(() => {
34
33
  s.updateGuideState,
35
34
  ]);
36
35
 
37
- const topics = useChatStore(
38
- (s) => [
39
- {
40
- favorite: false,
41
- id: 'default',
42
- title: t('topic.defaultTitle'),
43
- } as ChatTopic,
44
- ...topicSelectors.displayTopics(s),
36
+ const activeTopicList = useChatStore(topicSelectors.displayTopics, isEqual);
37
+
38
+ const topics = useMemo(
39
+ () => [
40
+ { favorite: false, id: 'default', title: t('topic.defaultTitle') } as ChatTopic,
41
+ ...(activeTopicList || []),
45
42
  ],
46
- isEqual,
43
+ [activeTopicList],
47
44
  );
48
45
 
49
46
  const [sessionId] = useSessionStore((s) => [s.activeId]);
50
47
 
51
- const { isLoading } = useFetchTopics(sessionId);
48
+ useFetchTopics(sessionId);
52
49
 
53
50
  const itemContent = useCallback(
54
51
  (index: number, { id, favorite, title }: ChatTopic) =>
@@ -62,13 +59,9 @@ const TopicListContent = memo(() => {
62
59
 
63
60
  const activeIndex = topics.findIndex((topic) => topic.id === activeTopicId);
64
61
 
65
- // first time loading
66
- if (!topicsInit) return <SkeletonList />;
67
-
68
- // in server mode and re-loading
69
- if (isServerMode && isLoading) return <SkeletonList />;
62
+ // first time loading or has no data
63
+ if (!topicsInit || !activeTopicList) return <SkeletonList />;
70
64
 
71
- // in client mode no need the loading state for better UX
72
65
  return (
73
66
  <>
74
67
  {topicLength === 0 && visible && (
@@ -111,8 +104,4 @@ const TopicListContent = memo(() => {
111
104
 
112
105
  TopicListContent.displayName = 'TopicListContent';
113
106
 
114
- export default memo(() => (
115
- <Suspense fallback={<SkeletonList />}>
116
- <TopicListContent />
117
- </Suspense>
118
- ));
107
+ export default TopicListContent;
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
 
3
- import { getClientConfig } from '../client';
3
+ import { getDebugConfig } from '../debug';
4
4
 
5
5
  // 测试前重置 process.env
6
6
  vi.stubGlobal('process', {
@@ -17,7 +17,7 @@ describe('getClientConfig', () => {
17
17
  process.env.NEXT_PUBLIC_I18N_DEBUG_BROWSER = '1';
18
18
  process.env.NEXT_PUBLIC_I18N_DEBUG_SERVER = '1';
19
19
 
20
- const config = getClientConfig();
20
+ const config = getDebugConfig();
21
21
  expect(config.I18N_DEBUG).toBe(true);
22
22
  expect(config.I18N_DEBUG_BROWSER).toBe(true);
23
23
  expect(config.I18N_DEBUG_SERVER).toBe(true);
@@ -31,7 +31,7 @@ describe('getClientConfig', () => {
31
31
  process.env.NEXT_PUBLIC_I18N_DEBUG_BROWSER = '0';
32
32
  process.env.NEXT_PUBLIC_I18N_DEBUG_SERVER = '0';
33
33
 
34
- const config = getClientConfig();
34
+ const config = getDebugConfig();
35
35
 
36
36
  expect(config.I18N_DEBUG).toBe(false);
37
37
  expect(config.I18N_DEBUG_BROWSER).toBe(false);
@@ -0,0 +1,16 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */
2
+ import { createEnv } from '@t3-oss/env-nextjs';
3
+ import { z } from 'zod';
4
+
5
+ export const getServerDBConfig = () => {
6
+ return createEnv({
7
+ client: {
8
+ NEXT_PUBLIC_ENABLED_SERVER_SERVICE: z.boolean(),
9
+ },
10
+ runtimeEnv: {
11
+ NEXT_PUBLIC_ENABLED_SERVER_SERVICE: process.env.NEXT_PUBLIC_SERVICE_MODE === 'server',
12
+ },
13
+ });
14
+ };
15
+
16
+ export const serverDBEnv = getServerDBConfig();
@@ -1,32 +1,22 @@
1
- /**
2
- * the client config is only used in Vercel deployment
3
- */
4
-
5
- /* eslint-disable sort-keys-fix/sort-keys-fix , typescript-sort-keys/interface */
6
-
7
1
  declare global {
8
2
  // eslint-disable-next-line @typescript-eslint/no-namespace
9
3
  namespace NodeJS {
10
4
  interface ProcessEnv {
5
+ NEXT_PUBLIC_DEVELOPER_DEBUG: string;
11
6
  NEXT_PUBLIC_I18N_DEBUG: string;
12
7
  NEXT_PUBLIC_I18N_DEBUG_BROWSER: string;
13
- NEXT_PUBLIC_I18N_DEBUG_SERVER: string;
14
-
15
- NEXT_PUBLIC_DEVELOPER_DEBUG: string;
16
8
 
17
- NEXT_PUBLIC_SERVICE_MODE?: 'server' | 'browser';
9
+ NEXT_PUBLIC_I18N_DEBUG_SERVER: string;
18
10
  }
19
11
  }
20
12
  }
21
13
 
22
- export const getClientConfig = () => ({
23
- ENABLED_SERVER_SERVICE: process.env.NEXT_PUBLIC_SERVICE_MODE === 'server',
14
+ export const getDebugConfig = () => ({
15
+ // developer debug mode
16
+ DEBUG_MODE: process.env.NEXT_PUBLIC_DEVELOPER_DEBUG === '1',
24
17
 
25
18
  // i18n debug mode
26
19
  I18N_DEBUG: process.env.NEXT_PUBLIC_I18N_DEBUG === '1',
27
20
  I18N_DEBUG_BROWSER: process.env.NEXT_PUBLIC_I18N_DEBUG_BROWSER === '1',
28
21
  I18N_DEBUG_SERVER: process.env.NEXT_PUBLIC_I18N_DEBUG_SERVER === '1',
29
-
30
- // developer debug mode
31
- DEBUG_MODE: process.env.NEXT_PUBLIC_DEVELOPER_DEBUG === '1',
32
22
  });
@@ -1,6 +1,6 @@
1
1
  import pkg from '@/../package.json';
2
- import { getClientConfig } from '@/config/client';
2
+ import { getServerDBConfig } from '@/config/db';
3
3
 
4
4
  export const CURRENT_VERSION = pkg.version;
5
5
 
6
- export const isServerMode = getClientConfig().ENABLED_SERVER_SERVICE;
6
+ export const isServerMode = getServerDBConfig().NEXT_PUBLIC_ENABLED_SERVER_SERVICE;
@@ -2,7 +2,7 @@ import dynamic from 'next/dynamic';
2
2
  import { cookies } from 'next/headers';
3
3
  import { FC, ReactNode } from 'react';
4
4
 
5
- import { getClientConfig } from '@/config/client';
5
+ import { getDebugConfig } from '@/config/debug';
6
6
  import { getServerFeatureFlagsValue } from '@/config/featureFlags';
7
7
  import { LOBE_LOCALE_COOKIE } from '@/const/locale';
8
8
  import {
@@ -26,7 +26,7 @@ let DebugUI: FC = () => null;
26
26
  // refs: https://webpack.js.org/plugins/internal-plugins/#constplugin
27
27
  if (process.env.NODE_ENV === 'development') {
28
28
  // eslint-disable-next-line unicorn/no-lonely-if
29
- if (getClientConfig().DEBUG_MODE) {
29
+ if (getDebugConfig().DEBUG_MODE) {
30
30
  DebugUI = dynamic(() => import('@/features/DebugUI'), { ssr: false }) as FC;
31
31
  }
32
32
  }
@@ -4,13 +4,13 @@ import resourcesToBackend from 'i18next-resources-to-backend';
4
4
  import { initReactI18next } from 'react-i18next';
5
5
  import { isRtlLang } from 'rtl-detect';
6
6
 
7
- import { getClientConfig } from '@/config/client';
7
+ import { getDebugConfig } from '@/config/debug';
8
8
  import { DEFAULT_LANG, LOBE_LOCALE_COOKIE } from '@/const/locale';
9
9
  import { COOKIE_CACHE_DAYS } from '@/const/settings';
10
10
  import { normalizeLocale } from '@/locales/resources';
11
11
  import { isDev, isOnServerSide } from '@/utils/env';
12
12
 
13
- const { I18N_DEBUG, I18N_DEBUG_BROWSER, I18N_DEBUG_SERVER } = getClientConfig();
13
+ const { I18N_DEBUG, I18N_DEBUG_BROWSER, I18N_DEBUG_SERVER } = getDebugConfig();
14
14
  const debugMode = I18N_DEBUG ?? isOnServerSide ? I18N_DEBUG_SERVER : I18N_DEBUG_BROWSER;
15
15
 
16
16
  export const createI18nNext = (lang?: string) => {
@@ -12,12 +12,14 @@ import { ChatTopic } from '@/types/topic';
12
12
 
13
13
  import { useChatStore } from '../../store';
14
14
 
15
+ vi.mock('zustand/traditional');
15
16
  // Mock topicService 和 messageService
16
17
  vi.mock('@/services/topic', () => ({
17
18
  topicService: {
18
19
  removeTopics: vi.fn(),
19
20
  removeAllTopic: vi.fn(),
20
21
  removeTopic: vi.fn(),
22
+ cloneTopic: vi.fn(),
21
23
  createTopic: vi.fn(),
22
24
  updateTopicFavorite: vi.fn(),
23
25
  updateTopicTitle: vi.fn(),
@@ -31,9 +33,23 @@ vi.mock('@/services/topic', () => ({
31
33
  vi.mock('@/services/message', () => ({
32
34
  messageService: {
33
35
  removeMessages: vi.fn(),
36
+ getMessages: vi.fn(),
34
37
  },
35
38
  }));
36
39
 
40
+ vi.mock('@/components/AntdStaticMethods', () => ({
41
+ message: {
42
+ loading: vi.fn(),
43
+ success: vi.fn(),
44
+ error: vi.fn(),
45
+ destroy: vi.fn(),
46
+ },
47
+ }));
48
+
49
+ vi.mock('i18next', () => ({
50
+ t: vi.fn((key, params) => (params.title ? key + '_' + params.title : key)),
51
+ }));
52
+
37
53
  beforeEach(() => {
38
54
  // Setup initial state and mocks before each test
39
55
  vi.clearAllMocks();
@@ -222,7 +238,7 @@ describe('topic action', () => {
222
238
  expect(result.current.data).toEqual(topics);
223
239
  });
224
240
  expect(useChatStore.getState().topicsInit).toBeTruthy();
225
- expect(useChatStore.getState().topics).toEqual(topics);
241
+ expect(useChatStore.getState().topicMaps).toEqual({ [sessionId]: topics });
226
242
  });
227
243
  });
228
244
  describe('useSearchTopics', () => {
@@ -411,7 +427,7 @@ describe('topic action', () => {
411
427
  const topics = [{ id: 'topic-1', title: 'Test Topic' }] as ChatTopic[];
412
428
  const { result } = renderHook(() => useChatStore());
413
429
  await act(async () => {
414
- useChatStore.setState({ topics });
430
+ useChatStore.setState({ topicMaps: { test: topics }, activeId: 'test' });
415
431
  });
416
432
 
417
433
  // Mock the `updateTopicTitleInSummary` and `refreshTopic` for spying
@@ -437,4 +453,82 @@ describe('topic action', () => {
437
453
  // TODO: need to test with fetchPresetTaskResult
438
454
  });
439
455
  });
456
+ describe('createTopic', () => {
457
+ it('should create a new topic and update the store', async () => {
458
+ const { result } = renderHook(() => useChatStore());
459
+ const activeId = 'test-session-id';
460
+ const newTopicId = 'new-topic-id';
461
+ const messages = [{ id: 'message-1' }, { id: 'message-2' }] as ChatMessage[];
462
+
463
+ await act(async () => {
464
+ useChatStore.setState({
465
+ activeId,
466
+ messagesMap: {
467
+ [messageMapKey(activeId)]: messages,
468
+ },
469
+ });
470
+ });
471
+
472
+ const createTopicSpy = vi.spyOn(topicService, 'createTopic').mockResolvedValue(newTopicId);
473
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
474
+
475
+ await act(async () => {
476
+ const topicId = await result.current.createTopic();
477
+ expect(topicId).toBe(newTopicId);
478
+ });
479
+
480
+ expect(createTopicSpy).toHaveBeenCalledWith({
481
+ sessionId: activeId,
482
+ messages: messages.map((m) => m.id),
483
+ title: 'topic.defaultTitle',
484
+ });
485
+ expect(refreshTopicSpy).toHaveBeenCalled();
486
+ });
487
+ });
488
+ describe('duplicateTopic', () => {
489
+ it('should duplicate a topic and switch to the new topic', async () => {
490
+ const { result } = renderHook(() => useChatStore());
491
+ const topicId = 'topic-1';
492
+ const newTopicId = 'new-topic-id';
493
+ const topics = [{ id: topicId, title: 'Original Topic' }] as ChatTopic[];
494
+
495
+ await act(async () => {
496
+ useChatStore.setState({ activeId: 'abc', topicMaps: { abc: topics } });
497
+ });
498
+
499
+ const cloneTopicSpy = vi.spyOn(topicService, 'cloneTopic').mockResolvedValue(newTopicId);
500
+ const refreshTopicSpy = vi.spyOn(result.current, 'refreshTopic');
501
+ const switchTopicSpy = vi.spyOn(result.current, 'switchTopic');
502
+
503
+ await act(async () => {
504
+ await result.current.duplicateTopic(topicId);
505
+ });
506
+
507
+ expect(cloneTopicSpy).toHaveBeenCalledWith(topicId, 'duplicateTitle_Original Topic');
508
+ expect(refreshTopicSpy).toHaveBeenCalled();
509
+ expect(switchTopicSpy).toHaveBeenCalledWith(newTopicId);
510
+ });
511
+ });
512
+ describe('autoRenameTopicTitle', () => {
513
+ it('should auto-rename the topic title based on the messages', async () => {
514
+ const { result } = renderHook(() => useChatStore());
515
+ const topicId = 'topic-1';
516
+ const activeId = 'test-session-id';
517
+ const messages = [{ id: 'message-1', content: 'Hello' }] as ChatMessage[];
518
+
519
+ await act(async () => {
520
+ useChatStore.setState({ activeId });
521
+ });
522
+
523
+ const getMessagesSpy = vi.spyOn(messageService, 'getMessages').mockResolvedValue(messages);
524
+ const summaryTopicTitleSpy = vi.spyOn(result.current, 'summaryTopicTitle');
525
+
526
+ await act(async () => {
527
+ await result.current.autoRenameTopicTitle(topicId);
528
+ });
529
+
530
+ expect(getMessagesSpy).toHaveBeenCalledWith(activeId, topicId);
531
+ expect(summaryTopicTitleSpy).toHaveBeenCalledWith(topicId, messages);
532
+ });
533
+ });
440
534
  });
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable sort-keys-fix/sort-keys-fix, typescript-sort-keys/interface */
2
2
  // Note: To make the code more logic and readable, we just disable the auto sort key eslint rule
3
3
  // DON'T REMOVE THE FIRST LINE
4
+ import isEqual from 'fast-deep-equal';
4
5
  import { t } from 'i18next';
5
6
  import { produce } from 'immer';
6
7
  import useSWR, { SWRResponse, mutate } from 'swr';
@@ -191,7 +192,16 @@ export const chatTopic: StateCreator<
191
192
  suspense: true,
192
193
  fallbackData: [],
193
194
  onSuccess: (topics) => {
194
- set({ topics, topicsInit: true }, false, n('useFetchTopics(success)', { sessionId }));
195
+ const nextMap = { ...get().topicMaps, [sessionId]: topics };
196
+
197
+ // no need to update map if the topics have been init and the map is the same
198
+ if (get().topicsInit && isEqual(nextMap, get().topicMaps)) return;
199
+
200
+ set(
201
+ { topicMaps: nextMap, topicsInit: true },
202
+ false,
203
+ n('useFetchTopics(success)', { sessionId }),
204
+ );
195
205
  },
196
206
  },
197
207
  ),
@@ -6,6 +6,7 @@ export interface ChatTopicState {
6
6
  isSearchingTopic: boolean;
7
7
  searchTopics: ChatTopic[];
8
8
  topicLoadingIds: string[];
9
+ topicMaps: Record<string, ChatTopic[]>;
9
10
  topicRenamingId?: string;
10
11
  topicSearchKeywords: string;
11
12
  topics: ChatTopic[];
@@ -20,6 +21,7 @@ export const initialTopicState: ChatTopicState = {
20
21
  isSearchingTopic: false,
21
22
  searchTopics: [],
22
23
  topicLoadingIds: [],
24
+ topicMaps: {},
23
25
  topicSearchKeywords: '',
24
26
  topics: [],
25
27
  topicsInit: false,
@@ -8,23 +8,25 @@ import { topicSelectors } from '../../selectors';
8
8
 
9
9
  const initialStore = initialState as ChatStore;
10
10
 
11
- const mockTopics = [
12
- { id: 'topic1', name: 'Topic 1' },
13
- { id: 'topic2', name: 'Topic 2' },
14
- ];
11
+ const topicMaps = {
12
+ test: [
13
+ { id: 'topic1', name: 'Topic 1', favorite: true },
14
+ { id: 'topic2', name: 'Topic 2' },
15
+ ],
16
+ };
15
17
 
16
18
  describe('topicSelectors', () => {
17
19
  describe('currentTopics', () => {
18
- it('should return an empty array if there are no topics', () => {
20
+ it('should return undefined if there are no topics with activeId', () => {
19
21
  const topics = topicSelectors.currentTopics(initialStore);
20
- expect(topics).toEqual([]);
22
+ expect(topics).toBeUndefined();
21
23
  });
22
24
 
23
25
  it('should return all current topics from the store', () => {
24
- const state = merge(initialStore, { topics: mockTopics });
26
+ const state = merge(initialStore, { topicMaps, activeId: 'test' });
25
27
 
26
28
  const topics = topicSelectors.currentTopics(state);
27
- expect(topics).toEqual(mockTopics);
29
+ expect(topics).toEqual(topicMaps.test);
28
30
  });
29
31
  });
30
32
 
@@ -35,9 +37,59 @@ describe('topicSelectors', () => {
35
37
  });
36
38
 
37
39
  it('should return the number of current topics', () => {
38
- const state = merge(initialStore, { topics: mockTopics });
40
+ const state = merge(initialStore, { topicMaps, activeId: 'test' });
39
41
  const length = topicSelectors.currentTopicLength(state);
40
- expect(length).toBe(mockTopics.length);
42
+ expect(length).toBe(topicMaps.test.length);
43
+ });
44
+ });
45
+
46
+ describe('currentActiveTopic', () => {
47
+ it('should return undefined if there is no active topic', () => {
48
+ const topic = topicSelectors.currentActiveTopic(initialStore);
49
+ expect(topic).toBeUndefined();
50
+ });
51
+
52
+ it('should return the current active topic', () => {
53
+ const state = merge(initialStore, { topicMaps, activeId: 'test', activeTopicId: 'topic1' });
54
+ const topic = topicSelectors.currentActiveTopic(state);
55
+ expect(topic).toEqual(topicMaps.test[0]);
56
+ });
57
+ });
58
+
59
+ describe('currentUnFavTopics', () => {
60
+ it('should return all unfavorited topics', () => {
61
+ const state = merge(initialStore, { topics: topicMaps.test });
62
+ const topics = topicSelectors.currentUnFavTopics(state);
63
+ expect(topics).toEqual([topicMaps.test[1]]);
64
+ });
65
+ });
66
+
67
+ describe('displayTopics', () => {
68
+ it('should return current topics if not searching', () => {
69
+ const state = merge(initialStore, { topicMaps, activeId: 'test' });
70
+ const topics = topicSelectors.displayTopics(state);
71
+ expect(topics).toEqual(topicMaps.test);
72
+ });
73
+
74
+ it('should return search topics if searching', () => {
75
+ const searchTopics = [{ id: 'search1', name: 'Search 1' }];
76
+ const state = merge(initialStore, { isSearchingTopic: true, searchTopics });
77
+ const topics = topicSelectors.displayTopics(state);
78
+ expect(topics).toEqual(searchTopics);
79
+ });
80
+ });
81
+
82
+ describe('getTopicById', () => {
83
+ it('should return undefined if topic is not found', () => {
84
+ const state = merge(initialStore, { topicMaps, activeId: 'test' });
85
+ const topic = topicSelectors.getTopicById('notfound')(state);
86
+ expect(topic).toBeUndefined();
87
+ });
88
+
89
+ it('should return the topic with the given id', () => {
90
+ const state = merge(initialStore, { topicMaps, activeId: 'test' });
91
+ const topic = topicSelectors.getTopicById('topic1')(state);
92
+ expect(topic).toEqual(topicMaps.test[0]);
41
93
  });
42
94
  });
43
95
  });
@@ -2,24 +2,24 @@ import { ChatTopic } from '@/types/topic';
2
2
 
3
3
  import { ChatStore } from '../../store';
4
4
 
5
- const currentTopics = (s: ChatStore): ChatTopic[] => s.topics;
5
+ const currentTopics = (s: ChatStore): ChatTopic[] | undefined => s.topicMaps[s.activeId];
6
6
 
7
7
  const currentActiveTopic = (s: ChatStore): ChatTopic | undefined => {
8
- return s.topics.find((topic) => topic.id === s.activeTopicId);
8
+ return currentTopics(s)?.find((topic) => topic.id === s.activeTopicId);
9
9
  };
10
10
  const searchTopics = (s: ChatStore): ChatTopic[] => s.searchTopics;
11
11
 
12
- const displayTopics = (s: ChatStore): ChatTopic[] =>
12
+ const displayTopics = (s: ChatStore): ChatTopic[] | undefined =>
13
13
  s.isSearchingTopic ? searchTopics(s) : currentTopics(s);
14
14
 
15
15
  const currentUnFavTopics = (s: ChatStore): ChatTopic[] => s.topics.filter((s) => !s.favorite);
16
16
 
17
- const currentTopicLength = (s: ChatStore): number => currentTopics(s).length;
17
+ const currentTopicLength = (s: ChatStore): number => currentTopics(s)?.length || 0;
18
18
 
19
19
  const getTopicById =
20
20
  (id: string) =>
21
21
  (s: ChatStore): ChatTopic | undefined =>
22
- currentTopics(s).find((topic) => topic.id === id);
22
+ currentTopics(s)?.find((topic) => topic.id === id);
23
23
 
24
24
  export const topicSelectors = {
25
25
  currentActiveTopic,