@lobehub/chat 0.161.4 → 0.161.6

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.6](https://github.com/lobehub/lobe-chat/compare/v0.161.5...v0.161.6)
6
+
7
+ <sup>Released on **2024-05-22**</sup>
8
+
9
+ <br/>
10
+
11
+ <details>
12
+ <summary><kbd>Improvements and Fixes</kbd></summary>
13
+
14
+ </details>
15
+
16
+ <div align="right">
17
+
18
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
19
+
20
+ </div>
21
+
22
+ ### [Version 0.161.5](https://github.com/lobehub/lobe-chat/compare/v0.161.4...v0.161.5)
23
+
24
+ <sup>Released on **2024-05-22**</sup>
25
+
26
+ #### ♻ Code Refactoring
27
+
28
+ - **misc**: Move feature flags ENV.
29
+
30
+ <br/>
31
+
32
+ <details>
33
+ <summary><kbd>Improvements and Fixes</kbd></summary>
34
+
35
+ #### Code refactoring
36
+
37
+ - **misc**: Move feature flags ENV, closes [#2605](https://github.com/lobehub/lobe-chat/issues/2605) ([054a404](https://github.com/lobehub/lobe-chat/commit/054a404))
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.4](https://github.com/lobehub/lobe-chat/compare/v0.161.3...v0.161.4)
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.4",
3
+ "version": "0.161.6",
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;
@@ -2,7 +2,7 @@ import { notFound } from 'next/navigation';
2
2
  import { PropsWithChildren } from 'react';
3
3
 
4
4
  import ServerLayout from '@/components/server/ServerLayout';
5
- import { serverFeatureFlags } from '@/config/server/featureFlags';
5
+ import { serverFeatureFlags } from '@/config/featureFlags';
6
6
 
7
7
  import Desktop from './_layout/Desktop';
8
8
  import Mobile from './_layout/Mobile';
@@ -1,6 +1,6 @@
1
1
  import { notFound } from 'next/navigation';
2
2
 
3
- import { serverFeatureFlags } from '@/config/server/featureFlags';
3
+ import { serverFeatureFlags } from '@/config/featureFlags';
4
4
  import { metadataModule } from '@/server/metadata';
5
5
  import { translation } from '@/server/translation';
6
6
 
@@ -1,6 +1,6 @@
1
1
  import { notFound } from 'next/navigation';
2
2
 
3
- import { serverFeatureFlags } from '@/config/server/featureFlags';
3
+ import { serverFeatureFlags } from '@/config/featureFlags';
4
4
  import { metadataModule } from '@/server/metadata';
5
5
  import { translation } from '@/server/translation';
6
6
  import { gerServerDeviceInfo, isMobileDevice } from '@/utils/responsive';
@@ -1,57 +1,31 @@
1
- /* eslint-disable sort-keys-fix/sort-keys-fix */
1
+ import { createEnv } from '@t3-oss/env-nextjs';
2
2
  import { z } from 'zod';
3
3
 
4
- export const FeatureFlagsSchema = z.object({
5
- webrtc_sync: z.boolean().optional(),
4
+ import { merge } from '@/utils/merge';
6
5
 
7
- language_model_settings: z.boolean().optional(),
6
+ import { DEFAULT_FEATURE_FLAGS, mapFeatureFlagsEnvToState } from './schema';
7
+ import { parseFeatureFlag } from './utils/parser';
8
8
 
9
- openai_api_key: z.boolean().optional(),
10
- openai_proxy_url: z.boolean().optional(),
9
+ const env = createEnv({
10
+ runtimeEnv: {
11
+ FEATURE_FLAGS: process.env.FEATURE_FLAGS,
12
+ },
11
13
 
12
- create_session: z.boolean().optional(),
13
- edit_agent: z.boolean().optional(),
14
-
15
- dalle: z.boolean().optional(),
16
-
17
- check_updates: z.boolean().optional(),
18
- welcome_suggest: z.boolean().optional(),
14
+ server: {
15
+ FEATURE_FLAGS: z.string().optional(),
16
+ },
19
17
  });
20
18
 
21
- // TypeScript 类型,从 Zod schema 生成
22
- export type IFeatureFlags = z.infer<typeof FeatureFlagsSchema>;
23
-
24
- export const DEFAULT_FEATURE_FLAGS: IFeatureFlags = {
25
- webrtc_sync: true,
26
-
27
- language_model_settings: true,
28
-
29
- openai_api_key: true,
30
- openai_proxy_url: true,
31
-
32
- create_session: true,
33
- edit_agent: true,
19
+ export const getServerFeatureFlagsValue = () => {
20
+ const flags = parseFeatureFlag(env.FEATURE_FLAGS);
34
21
 
35
- dalle: true,
36
-
37
- check_updates: true,
38
- welcome_suggest: true,
22
+ return merge(DEFAULT_FEATURE_FLAGS, flags);
39
23
  };
40
24
 
41
- export const mapFeatureFlagsEnvToState = (config: IFeatureFlags) => {
42
- return {
43
- enableWebrtc: config.webrtc_sync,
44
- isAgentEditable: config.edit_agent,
45
-
46
- showCreateSession: config.create_session,
47
- showLLM: config.language_model_settings,
48
-
49
- showOpenAIApiKey: config.openai_api_key,
50
- showOpenAIProxyUrl: config.openai_proxy_url,
25
+ export const serverFeatureFlags = () => {
26
+ const serverConfig = getServerFeatureFlagsValue();
51
27
 
52
- showDalle: config.dalle,
53
-
54
- enableCheckUpdates: config.check_updates,
55
- showWelcomeSuggest: config.welcome_suggest,
56
- };
28
+ return mapFeatureFlagsEnvToState(serverConfig);
57
29
  };
30
+
31
+ export * from './schema';
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { FeatureFlagsSchema, mapFeatureFlagsEnvToState } from './index';
3
+ import { FeatureFlagsSchema, mapFeatureFlagsEnvToState } from './schema';
4
4
 
5
5
  describe('FeatureFlagsSchema', () => {
6
6
  it('should validate correct feature flags', () => {
@@ -0,0 +1,57 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
2
+ import { z } from 'zod';
3
+
4
+ export const FeatureFlagsSchema = z.object({
5
+ webrtc_sync: z.boolean().optional(),
6
+
7
+ language_model_settings: z.boolean().optional(),
8
+
9
+ openai_api_key: z.boolean().optional(),
10
+ openai_proxy_url: z.boolean().optional(),
11
+
12
+ create_session: z.boolean().optional(),
13
+ edit_agent: z.boolean().optional(),
14
+
15
+ dalle: z.boolean().optional(),
16
+
17
+ check_updates: z.boolean().optional(),
18
+ welcome_suggest: z.boolean().optional(),
19
+ });
20
+
21
+ // TypeScript 类型,从 Zod schema 生成
22
+ export type IFeatureFlags = z.infer<typeof FeatureFlagsSchema>;
23
+
24
+ export const DEFAULT_FEATURE_FLAGS: IFeatureFlags = {
25
+ webrtc_sync: true,
26
+
27
+ language_model_settings: true,
28
+
29
+ openai_api_key: true,
30
+ openai_proxy_url: true,
31
+
32
+ create_session: true,
33
+ edit_agent: true,
34
+
35
+ dalle: true,
36
+
37
+ check_updates: true,
38
+ welcome_suggest: true,
39
+ };
40
+
41
+ export const mapFeatureFlagsEnvToState = (config: IFeatureFlags) => {
42
+ return {
43
+ enableWebrtc: config.webrtc_sync,
44
+ isAgentEditable: config.edit_agent,
45
+
46
+ showCreateSession: config.create_session,
47
+ showLLM: config.language_model_settings,
48
+
49
+ showOpenAIApiKey: config.openai_api_key,
50
+ showOpenAIProxyUrl: config.openai_proxy_url,
51
+
52
+ showDalle: config.dalle,
53
+
54
+ enableCheckUpdates: config.check_updates,
55
+ showWelcomeSuggest: config.welcome_suggest,
56
+ };
57
+ };
@@ -1,4 +1,4 @@
1
- import { FeatureFlagsSchema, IFeatureFlags } from '@/config/featureFlags';
1
+ import { FeatureFlagsSchema, IFeatureFlags } from '../schema';
2
2
 
3
3
  /**
4
4
  * 解析环境变量中的特性标志字符串。
@@ -3,7 +3,7 @@ import { cookies } from 'next/headers';
3
3
  import { FC, ReactNode } from 'react';
4
4
 
5
5
  import { getClientConfig } from '@/config/client';
6
- import { getServerFeatureFlagsValue } from '@/config/server/featureFlags';
6
+ import { getServerFeatureFlagsValue } from '@/config/featureFlags';
7
7
  import { LOBE_LOCALE_COOKIE } from '@/const/locale';
8
8
  import {
9
9
  LOBE_THEME_APPEARANCE,
@@ -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,
@@ -1,29 +0,0 @@
1
- import { createEnv } from '@t3-oss/env-nextjs';
2
- import { z } from 'zod';
3
-
4
- import { DEFAULT_FEATURE_FLAGS, mapFeatureFlagsEnvToState } from '@/config/featureFlags';
5
- import { merge } from '@/utils/merge';
6
-
7
- import { parseFeatureFlag } from './parser';
8
-
9
- const env = createEnv({
10
- runtimeEnv: {
11
- FEATURE_FLAGS: process.env.FEATURE_FLAGS,
12
- },
13
-
14
- server: {
15
- FEATURE_FLAGS: z.string().optional(),
16
- },
17
- });
18
-
19
- export const getServerFeatureFlagsValue = () => {
20
- const flags = parseFeatureFlag(env.FEATURE_FLAGS);
21
-
22
- return merge(DEFAULT_FEATURE_FLAGS, flags);
23
- };
24
-
25
- export const serverFeatureFlags = () => {
26
- const serverConfig = getServerFeatureFlagsValue();
27
-
28
- return mapFeatureFlagsEnvToState(serverConfig);
29
- };