@lobehub/chat 1.133.5 → 1.134.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/apps/desktop/src/main/appBrowsers.ts +51 -0
  3. package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +72 -1
  4. package/apps/desktop/src/main/core/browser/BrowserManager.ts +88 -18
  5. package/changelog/v1.json +21 -0
  6. package/package.json +1 -1
  7. package/packages/database/src/models/__tests__/aiModel.test.ts +3 -0
  8. package/packages/database/src/models/aiModel.ts +18 -2
  9. package/packages/electron-client-ipc/src/events/windows.ts +39 -0
  10. package/packages/model-bank/src/aiModels/google.ts +64 -2
  11. package/packages/model-bank/src/types/aiModel.ts +13 -9
  12. package/packages/model-runtime/src/providers/google/createImage.ts +13 -4
  13. package/src/app/[variants]/(main)/_layout/Desktop/DesktopLayoutContainer.tsx +4 -2
  14. package/src/app/[variants]/(main)/_layout/Desktop/SideBar/index.tsx +3 -1
  15. package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +3 -1
  16. package/src/app/[variants]/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicItem/TopicContent.tsx +25 -1
  17. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +19 -2
  18. package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +27 -1
  19. package/src/app/[variants]/(main)/chat/_layout/Desktop/SessionPanel.tsx +11 -1
  20. package/src/app/[variants]/(main)/chat/features/TogglePanelButton.tsx +6 -0
  21. package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +6 -3
  22. package/src/config/featureFlags/index.ts +2 -2
  23. package/src/config/featureFlags/schema.test.ts +165 -9
  24. package/src/config/featureFlags/schema.ts +68 -46
  25. package/src/features/ElectronTitlebar/Connection/index.tsx +0 -1
  26. package/src/hooks/useIsSingleMode.test.ts +66 -0
  27. package/src/hooks/useIsSingleMode.ts +29 -0
  28. package/src/server/featureFlags/index.ts +56 -0
  29. package/src/server/modules/EdgeConfig/index.ts +43 -4
  30. package/src/server/routers/lambda/aiModel.test.ts +2 -0
  31. package/src/store/global/actions/general.ts +46 -0
@@ -1,4 +1,4 @@
1
- import { Content, GoogleGenAI, Part } from '@google/genai';
1
+ import { Content, GenerateContentConfig, GoogleGenAI, Part } from '@google/genai';
2
2
 
3
3
  import { convertGoogleAIUsage } from '../../core/usageConverters/google-ai';
4
4
  import { CreateImagePayload, CreateImageResponse } from '../../types/image';
@@ -141,10 +141,19 @@ async function generateImageByChatModel(
141
141
  },
142
142
  ];
143
143
 
144
+ const config: GenerateContentConfig = {
145
+ responseModalities: ['Image'],
146
+ ...(params.aspectRatio
147
+ ? {
148
+ imageConfig: {
149
+ aspectRatio: params.aspectRatio,
150
+ },
151
+ }
152
+ : {}),
153
+ };
154
+
144
155
  const response = await client.models.generateContent({
145
- config: {
146
- responseModalities: ['Image'],
147
- },
156
+ config,
148
157
  contents,
149
158
  model: actualModel,
150
159
  });
@@ -1,6 +1,6 @@
1
1
  import { useTheme } from 'antd-style';
2
2
  import { usePathname } from 'next/navigation';
3
- import { PropsWithChildren, memo } from 'react';
3
+ import { PropsWithChildren, Suspense, memo } from 'react';
4
4
  import { Flexbox } from 'react-layout-kit';
5
5
 
6
6
  import SideBar from './SideBar';
@@ -11,7 +11,9 @@ const DesktopLayoutContainer = memo<PropsWithChildren>(({ children }) => {
11
11
  const hideSideBar = pathname.startsWith('/settings');
12
12
  return (
13
13
  <>
14
- {!hideSideBar && <SideBar />}
14
+ <Suspense>
15
+ {!hideSideBar && <SideBar />}
16
+ </Suspense>
15
17
  <Flexbox
16
18
  style={{
17
19
  background: theme.colorBgLayout,
@@ -6,6 +6,7 @@ import { Suspense, memo } from 'react';
6
6
 
7
7
  import { isDesktop } from '@/const/version';
8
8
  import { useActiveTabKey } from '@/hooks/useActiveTabKey';
9
+ import { useIsSingleMode } from '@/hooks/useIsSingleMode';
9
10
  import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
10
11
  import { useGlobalStore } from '@/store/global';
11
12
  import { systemStatusSelectors } from '@/store/global/selectors';
@@ -26,11 +27,12 @@ const Top = () => {
26
27
 
27
28
  const Nav = memo(() => {
28
29
  const theme = useTheme();
30
+ const isSingleMode = useIsSingleMode()
29
31
  const inZenMode = useGlobalStore(systemStatusSelectors.inZenMode);
30
32
  const { showPinList } = useServerConfigStore(featureFlagsSelectors);
31
33
 
32
34
  return (
33
- !inZenMode && (
35
+ !inZenMode && !isSingleMode && (
34
36
  <SideNav
35
37
  avatar={
36
38
  <div className={electronStylish.nodrag}>
@@ -48,7 +48,9 @@ const Layout = memo<PropsWithChildren>(({ children }) => {
48
48
  <DesktopLayoutContainer>{children}</DesktopLayoutContainer>
49
49
  ) : (
50
50
  <>
51
- <SideBar />
51
+ <Suspense>
52
+ <SideBar />
53
+ </Suspense>
52
54
  {children}
53
55
  </>
54
56
  )}
@@ -2,6 +2,7 @@ import { ActionIcon, Dropdown, EditableText, Icon, type MenuProps, Text } from '
2
2
  import { App } from 'antd';
3
3
  import { createStyles } from 'antd-style';
4
4
  import {
5
+ ExternalLink,
5
6
  LucideCopy,
6
7
  LucideLoader2,
7
8
  MoreVertical,
@@ -16,8 +17,10 @@ import { Flexbox } from 'react-layout-kit';
16
17
 
17
18
  import BubblesLoading from '@/components/BubblesLoading';
18
19
  import { LOADING_FLAT } from '@/const/message';
20
+ import { isDesktop } from '@/const/version';
19
21
  import { useIsMobile } from '@/hooks/useIsMobile';
20
22
  import { useChatStore } from '@/store/chat';
23
+ import { useGlobalStore } from '@/store/global';
21
24
 
22
25
  const useStyles = createStyles(({ css }) => ({
23
26
  content: css`
@@ -45,6 +48,8 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
45
48
 
46
49
  const mobile = useIsMobile();
47
50
 
51
+ const openTopicInNewWindow = useGlobalStore((s) => s.openTopicInNewWindow);
52
+
48
53
  const [
49
54
  editing,
50
55
  favoriteTopic,
@@ -53,6 +58,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
53
58
  autoRenameTopicTitle,
54
59
  duplicateTopic,
55
60
  isLoading,
61
+ activeId,
56
62
  ] = useChatStore((s) => [
57
63
  s.topicRenamingId === id,
58
64
  s.favoriteTopic,
@@ -61,6 +67,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
61
67
  s.autoRenameTopicTitle,
62
68
  s.duplicateTopic,
63
69
  s.topicLoadingIds.includes(id),
70
+ s.activeId,
64
71
  ]);
65
72
  const { styles, theme } = useStyles();
66
73
 
@@ -88,6 +95,18 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
88
95
  toggleEditing(true);
89
96
  },
90
97
  },
98
+ ...(isDesktop
99
+ ? [
100
+ {
101
+ icon: <Icon icon={ExternalLink} />,
102
+ key: 'openInNewWindow',
103
+ label: '单独打开页面',
104
+ onClick: () => {
105
+ openTopicInNewWindow(activeId, id);
106
+ },
107
+ },
108
+ ]
109
+ : []),
91
110
  {
92
111
  type: 'divider',
93
112
  },
@@ -134,7 +153,7 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
134
153
  },
135
154
  },
136
155
  ],
137
- [],
156
+ [id, activeId, autoRenameTopicTitle, duplicateTopic, removeTopic, t, toggleEditing, openTopicInNewWindow],
138
157
  );
139
158
 
140
159
  return (
@@ -169,6 +188,11 @@ const TopicContent = memo<TopicContentProps>(({ id, title, fav, showMore }) => {
169
188
  <Text
170
189
  className={styles.title}
171
190
  ellipsis={{ rows: 1, tooltip: { placement: 'left', title } }}
191
+ onDoubleClick={() => {
192
+ if (isDesktop) {
193
+ openTopicInNewWindow(activeId, id)
194
+ }
195
+ }}
172
196
  style={{ margin: 0 }}
173
197
  >
174
198
  {title}
@@ -5,6 +5,7 @@ import { ItemType } from 'antd/es/menu/interface';
5
5
  import isEqual from 'fast-deep-equal';
6
6
  import {
7
7
  Check,
8
+ ExternalLink,
8
9
  HardDriveDownload,
9
10
  ListTree,
10
11
  LucideCopy,
@@ -17,8 +18,9 @@ import {
17
18
  import { memo, useMemo } from 'react';
18
19
  import { useTranslation } from 'react-i18next';
19
20
 
20
- import { isServerMode } from '@/const/version';
21
+ import { isDesktop, isServerMode } from '@/const/version';
21
22
  import { configService } from '@/services/config';
23
+ import { useGlobalStore } from '@/store/global';
22
24
  import { useSessionStore } from '@/store/session';
23
25
  import { sessionHelpers } from '@/store/session/helpers';
24
26
  import { sessionGroupSelectors, sessionSelectors } from '@/store/session/selectors';
@@ -41,6 +43,8 @@ const Actions = memo<ActionProps>(({ group, id, openCreateGroupModal, setOpen })
41
43
  const { styles } = useStyles();
42
44
  const { t } = useTranslation('chat');
43
45
 
46
+ const openSessionInNewWindow = useGlobalStore((s) => s.openSessionInNewWindow);
47
+
44
48
  const sessionCustomGroups = useSessionStore(sessionGroupSelectors.sessionGroupItems, isEqual);
45
49
  const [pin, removeSession, pinSession, duplicateSession, updateSessionGroup] = useSessionStore(
46
50
  (s) => {
@@ -82,6 +86,19 @@ const Actions = memo<ActionProps>(({ group, id, openCreateGroupModal, setOpen })
82
86
  duplicateSession(id);
83
87
  },
84
88
  },
89
+ ...(isDesktop
90
+ ? [
91
+ {
92
+ icon: <Icon icon={ExternalLink} />,
93
+ key: 'openInNewWindow',
94
+ label: '单独打开页面',
95
+ onClick: ({ domEvent }: { domEvent: Event }) => {
96
+ domEvent.stopPropagation();
97
+ openSessionInNewWindow(id);
98
+ },
99
+ },
100
+ ]
101
+ : []),
85
102
  {
86
103
  type: 'divider',
87
104
  },
@@ -167,7 +184,7 @@ const Actions = memo<ActionProps>(({ group, id, openCreateGroupModal, setOpen })
167
184
  },
168
185
  ] as ItemType[]
169
186
  ).filter(Boolean),
170
- [id, pin],
187
+ [id, pin, openSessionInNewWindow],
171
188
  );
172
189
 
173
190
  return (
@@ -1,12 +1,14 @@
1
1
  import { ModelTag } from '@lobehub/icons';
2
- import { memo, useMemo, useState } from 'react';
2
+ import React, { memo, useMemo, useState } from 'react';
3
3
  import { Flexbox } from 'react-layout-kit';
4
4
  import { shallow } from 'zustand/shallow';
5
5
 
6
+ import { isDesktop } from '@/const/version';
6
7
  import { useAgentStore } from '@/store/agent';
7
8
  import { agentSelectors } from '@/store/agent/selectors';
8
9
  import { useChatStore } from '@/store/chat';
9
10
  import { chatSelectors } from '@/store/chat/selectors';
11
+ import { useGlobalStore } from '@/store/global';
10
12
  import { useSessionStore } from '@/store/session';
11
13
  import { sessionHelpers } from '@/store/session/helpers';
12
14
  import { sessionMetaSelectors, sessionSelectors } from '@/store/session/selectors';
@@ -24,6 +26,8 @@ const SessionItem = memo<SessionItemProps>(({ id }) => {
24
26
  const [createGroupModalOpen, setCreateGroupModalOpen] = useState(false);
25
27
  const [defaultModel] = useAgentStore((s) => [agentSelectors.inboxAgentModel(s)]);
26
28
 
29
+ const openSessionInNewWindow = useGlobalStore((s) => s.openSessionInNewWindow);
30
+
27
31
  const [active] = useSessionStore((s) => [s.activeId === id]);
28
32
  const [loading] = useChatStore((s) => [chatSelectors.isAIGenerating(s) && id === s.activeId]);
29
33
 
@@ -46,6 +50,24 @@ const SessionItem = memo<SessionItemProps>(({ id }) => {
46
50
 
47
51
  const showModel = model !== defaultModel;
48
52
 
53
+ const handleDoubleClick = () => {
54
+ if (isDesktop) {
55
+ openSessionInNewWindow(id);
56
+ }
57
+ };
58
+
59
+ const handleDragStart = (e: React.DragEvent) => {
60
+ // Set drag data to identify the session being dragged
61
+ e.dataTransfer.setData('text/plain', id);
62
+ };
63
+
64
+ const handleDragEnd = (e: React.DragEvent) => {
65
+ // If drag ends without being dropped in a valid target, open in new window
66
+ if (isDesktop && e.dataTransfer.dropEffect === 'none') {
67
+ openSessionInNewWindow(id);
68
+ }
69
+ };
70
+
49
71
  const actions = useMemo(
50
72
  () => (
51
73
  <Actions
@@ -78,8 +100,12 @@ const SessionItem = memo<SessionItemProps>(({ id }) => {
78
100
  avatarBackground={avatarBackground}
79
101
  date={updateAt?.valueOf()}
80
102
  description={description}
103
+ draggable={isDesktop}
81
104
  key={id}
82
105
  loading={loading}
106
+ onDoubleClick={handleDoubleClick}
107
+ onDragEnd={handleDragEnd}
108
+ onDragStart={handleDragStart}
83
109
  pin={pin}
84
110
  showAction={open}
85
111
  styles={{
@@ -7,6 +7,7 @@ import { PropsWithChildren, memo, useEffect, useMemo, useState } from 'react';
7
7
 
8
8
  import { withSuspense } from '@/components/withSuspense';
9
9
  import { FOLDER_WIDTH } from '@/const/layoutTokens';
10
+ import { useIsSingleMode } from '@/hooks/useIsSingleMode';
10
11
  import { usePinnedAgentState } from '@/hooks/usePinnedAgentState';
11
12
  import { useGlobalStore } from '@/store/global';
12
13
  import { systemStatusSelectors } from '@/store/global/selectors';
@@ -33,11 +34,14 @@ export const useStyles = createStyles(({ css, token }) => ({
33
34
  }));
34
35
 
35
36
  const SessionPanel = memo<PropsWithChildren>(({ children }) => {
37
+ const isSingleMode = useIsSingleMode();
38
+
36
39
  const { md = true } = useResponsive();
37
40
 
38
41
  const [isPinned] = usePinnedAgentState();
39
42
 
40
43
  const { styles } = useStyles();
44
+
41
45
  const [sessionsWidth, sessionExpandable, updatePreference] = useGlobalStore((s) => [
42
46
  systemStatusSelectors.sessionWidth(s),
43
47
  systemStatusSelectors.showSessionPanel(s),
@@ -72,6 +76,12 @@ const SessionPanel = memo<PropsWithChildren>(({ children }) => {
72
76
  const { appearance } = useThemeMode();
73
77
 
74
78
  const SessionPanel = useMemo(() => {
79
+ if (isSingleMode) {
80
+ // 在单一模式下,仍然渲染 children 以确保 SessionHydration 等逻辑组件正常工作
81
+ // 但使用隐藏样式而不是 return null
82
+ return <div style={{ display: 'none' }}>{children}</div>;
83
+ }
84
+
75
85
  return (
76
86
  <DraggablePanel
77
87
  className={styles.panel}
@@ -92,7 +102,7 @@ const SessionPanel = memo<PropsWithChildren>(({ children }) => {
92
102
  </DraggablePanelContainer>
93
103
  </DraggablePanel>
94
104
  );
95
- }, [sessionsWidth, md, isPinned, sessionExpandable, tmpWidth, appearance]);
105
+ }, [sessionsWidth, md, isPinned, sessionExpandable, tmpWidth, appearance, isSingleMode]);
96
106
 
97
107
  return SessionPanel;
98
108
  });
@@ -6,6 +6,7 @@ import { memo } from 'react';
6
6
  import { useTranslation } from 'react-i18next';
7
7
 
8
8
  import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens';
9
+ import { useIsSingleMode } from '@/hooks/useIsSingleMode';
9
10
  import { useGlobalStore } from '@/store/global';
10
11
  import { systemStatusSelectors } from '@/store/global/selectors';
11
12
  import { useUserStore } from '@/store/user';
@@ -15,6 +16,7 @@ import { HotkeyEnum } from '@/types/hotkey';
15
16
  export const TOOGLE_PANEL_BUTTON_ID = 'toggle-panel-button';
16
17
 
17
18
  const TogglePanelButton = memo(() => {
19
+ const isSingleMode = useIsSingleMode();
18
20
  const hotkey = useUserStore(settingsSelectors.getHotkeyById(HotkeyEnum.ToggleLeftPanel));
19
21
 
20
22
  const { t } = useTranslation(['chat', 'hotkey']);
@@ -22,6 +24,10 @@ const TogglePanelButton = memo(() => {
22
24
  const showSessionPanel = useGlobalStore(systemStatusSelectors.showSessionPanel);
23
25
  const updateSystemStatus = useGlobalStore((s) => s.updateSystemStatus);
24
26
 
27
+ if (isSingleMode) {
28
+ return null
29
+ }
30
+
25
31
  return (
26
32
  <Tooltip hotkey={hotkey} title={t('toggleLeftPanel.title', { ns: 'hotkey' })}>
27
33
  <ActionIcon
@@ -10,7 +10,6 @@ import { Flexbox } from 'react-layout-kit';
10
10
  import { ModelInfoTags } from '@/components/ModelSelect';
11
11
  import { useIsMobile } from '@/hooks/useIsMobile';
12
12
  import { aiModelSelectors, useAiInfraStore } from '@/store/aiInfra';
13
- import { AiModelSourceEnum, AiProviderModelListItem } from '../../../../../../../../packages/model-bank/src/types/aiModel';
14
13
  import { formatPriceByCurrency } from '@/utils/format';
15
14
  import {
16
15
  getAudioInputUnitRate,
@@ -18,6 +17,10 @@ import {
18
17
  getTextOutputUnitRate,
19
18
  } from '@/utils/pricing';
20
19
 
20
+ import {
21
+ AiModelSourceEnum,
22
+ AiProviderModelListItem,
23
+ } from '../../../../../../../../packages/model-bank/src/types/aiModel';
21
24
  import ModelConfigModal from './ModelConfigModal';
22
25
  import { ProviderSettingsContext } from './ProviderSettingsContext';
23
26
 
@@ -243,7 +246,7 @@ const ModelItem = memo<ModelItemProps>(
243
246
  loading={isModelLoading}
244
247
  onChange={async (e) => {
245
248
  setChecked(e);
246
- await toggleModelEnabled({ enabled: e, id, source });
249
+ await toggleModelEnabled({ enabled: e, id, source, type });
247
250
  }}
248
251
  size={'small'}
249
252
  />
@@ -334,7 +337,7 @@ const ModelItem = memo<ModelItemProps>(
334
337
  loading={isModelLoading}
335
338
  onChange={async (e) => {
336
339
  setChecked(e);
337
- await toggleModelEnabled({ enabled: e, id, source });
340
+ await toggleModelEnabled({ enabled: e, id, source, type });
338
341
  }}
339
342
  size={'small'}
340
343
  />
@@ -22,10 +22,10 @@ export const getServerFeatureFlagsValue = () => {
22
22
  return merge(DEFAULT_FEATURE_FLAGS, flags);
23
23
  };
24
24
 
25
- export const serverFeatureFlags = () => {
25
+ export const serverFeatureFlags = (userId?: string) => {
26
26
  const serverConfig = getServerFeatureFlagsValue();
27
27
 
28
- return mapFeatureFlagsEnvToState(serverConfig);
28
+ return mapFeatureFlagsEnvToState(serverConfig, userId);
29
29
  };
30
30
 
31
31
  export * from './schema';
@@ -1,11 +1,10 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
 
3
- import { FeatureFlagsSchema, mapFeatureFlagsEnvToState } from './schema';
3
+ import { FeatureFlagsSchema, evaluateFeatureFlag, mapFeatureFlagsEnvToState } from './schema';
4
4
 
5
5
  describe('FeatureFlagsSchema', () => {
6
- it('should validate correct feature flags', () => {
6
+ it('should validate correct feature flags with boolean values', () => {
7
7
  const result = FeatureFlagsSchema.safeParse({
8
- webrtc_sync: true,
9
8
  language_model_settings: false,
10
9
  openai_api_key: true,
11
10
  openai_proxy_url: false,
@@ -18,20 +17,89 @@ describe('FeatureFlagsSchema', () => {
18
17
  expect(result.success).toBe(true);
19
18
  });
20
19
 
21
- it('should reject invalid feature flags', () => {
20
+ it('should validate correct feature flags with user ID arrays', () => {
22
21
  const result = FeatureFlagsSchema.safeParse({
23
- edit_agent: 'yes', // Invalid type, should be boolean
22
+ edit_agent: ['user-123', 'user-456'],
23
+ create_session: ['user-789'],
24
+ dalle: true,
25
+ ai_image: false,
26
+ });
27
+
28
+ expect(result.success).toBe(true);
29
+ });
30
+
31
+ it('should validate mixed boolean and array values', () => {
32
+ const result = FeatureFlagsSchema.safeParse({
33
+ edit_agent: ['user-123'],
34
+ create_session: true,
35
+ dalle: false,
36
+ knowledge_base: ['user-456', 'user-789'],
37
+ });
38
+
39
+ expect(result.success).toBe(true);
40
+ });
41
+
42
+ it('should reject invalid feature flags with wrong types', () => {
43
+ const result = FeatureFlagsSchema.safeParse({
44
+ edit_agent: 'yes', // Invalid type, should be boolean or array
45
+ });
46
+
47
+ expect(result.success).toBe(false);
48
+ });
49
+
50
+ it('should reject invalid feature flags with non-string array elements', () => {
51
+ const result = FeatureFlagsSchema.safeParse({
52
+ edit_agent: [123, 456], // Invalid, array should contain strings
24
53
  });
25
54
 
26
55
  expect(result.success).toBe(false);
27
56
  });
28
57
  });
29
58
 
59
+ describe('evaluateFeatureFlag', () => {
60
+ it('should return true for boolean true value', () => {
61
+ expect(evaluateFeatureFlag(true)).toBe(true);
62
+ expect(evaluateFeatureFlag(true, 'user-123')).toBe(true);
63
+ });
64
+
65
+ it('should return false for boolean false value', () => {
66
+ expect(evaluateFeatureFlag(false)).toBe(false);
67
+ expect(evaluateFeatureFlag(false, 'user-123')).toBe(false);
68
+ });
69
+
70
+ it('should return undefined for undefined value', () => {
71
+ expect(evaluateFeatureFlag(undefined)).toBe(undefined);
72
+ expect(evaluateFeatureFlag(undefined, 'user-123')).toBe(undefined);
73
+ });
74
+
75
+ it('should return true if user ID is in the allowlist', () => {
76
+ const allowlist = ['user-123', 'user-456'];
77
+ expect(evaluateFeatureFlag(allowlist, 'user-123')).toBe(true);
78
+ expect(evaluateFeatureFlag(allowlist, 'user-456')).toBe(true);
79
+ });
80
+
81
+ it('should return false if user ID is not in the allowlist', () => {
82
+ const allowlist = ['user-123', 'user-456'];
83
+ expect(evaluateFeatureFlag(allowlist, 'user-789')).toBe(false);
84
+ });
85
+
86
+ it('should return false if no user ID provided with array value', () => {
87
+ const allowlist = ['user-123', 'user-456'];
88
+ expect(evaluateFeatureFlag(allowlist)).toBe(false);
89
+ expect(evaluateFeatureFlag(allowlist, undefined)).toBe(false);
90
+ });
91
+
92
+ it('should handle empty array', () => {
93
+ expect(evaluateFeatureFlag([], 'user-123')).toBe(false);
94
+ expect(evaluateFeatureFlag([])).toBe(false);
95
+ });
96
+ });
97
+
30
98
  describe('mapFeatureFlagsEnvToState', () => {
31
- it('should correctly map feature flags to state', () => {
99
+ it('should correctly map boolean feature flags to state', () => {
32
100
  const config = {
33
- webrtc_sync: true,
34
101
  language_model_settings: false,
102
+ provider_settings: true,
35
103
  openai_api_key: true,
36
104
  openai_proxy_url: false,
37
105
  create_session: true,
@@ -40,22 +108,110 @@ describe('mapFeatureFlagsEnvToState', () => {
40
108
  ai_image: true,
41
109
  check_updates: true,
42
110
  welcome_suggest: true,
111
+ plugins: true,
112
+ knowledge_base: false,
113
+ rag_eval: true,
114
+ clerk_sign_up: false,
115
+ market: true,
116
+ speech_to_text: true,
117
+ changelog: false,
118
+ pin_list: true,
119
+ api_key_manage: false,
120
+ cloud_promotion: true,
121
+ commercial_hide_github: false,
122
+ commercial_hide_docs: true,
43
123
  };
44
124
 
45
- const expectedState = {
125
+ const mappedState = mapFeatureFlagsEnvToState(config);
126
+
127
+ expect(mappedState).toMatchObject({
46
128
  isAgentEditable: false,
47
129
  showCreateSession: true,
48
130
  showLLM: false,
131
+ showProvider: true,
49
132
  showOpenAIApiKey: true,
50
133
  showOpenAIProxyUrl: false,
51
134
  showDalle: true,
52
135
  showAiImage: true,
53
136
  enableCheckUpdates: true,
54
137
  showWelcomeSuggest: true,
138
+ enablePlugins: true,
139
+ enableKnowledgeBase: false,
140
+ enableRAGEval: true,
141
+ enableClerkSignUp: false,
142
+ showMarket: true,
143
+ enableSTT: true,
144
+ showChangelog: false,
145
+ showPinList: true,
146
+ showApiKeyManage: false,
147
+ showCloudPromotion: true,
148
+ hideGitHub: false,
149
+ hideDocs: true,
150
+ });
151
+ });
152
+
153
+ it('should correctly evaluate user-specific flags with allowlist', () => {
154
+ const userId = 'user-123';
155
+ const config = {
156
+ edit_agent: ['user-123', 'user-456'],
157
+ create_session: ['user-789'],
158
+ dalle: true,
159
+ knowledge_base: ['user-123'],
160
+ };
161
+
162
+ const mappedState = mapFeatureFlagsEnvToState(config, userId);
163
+
164
+ expect(mappedState.isAgentEditable).toBe(true); // user-123 is in allowlist
165
+ expect(mappedState.showCreateSession).toBe(false); // user-123 is not in allowlist
166
+ expect(mappedState.showDalle).toBe(true); // boolean true
167
+ expect(mappedState.enableKnowledgeBase).toBe(true); // user-123 is in allowlist
168
+ });
169
+
170
+ it('should return false for array flags when user ID is not in allowlist', () => {
171
+ const userId = 'user-999';
172
+ const config = {
173
+ edit_agent: ['user-123', 'user-456'],
174
+ create_session: ['user-789'],
175
+ dalle: true,
176
+ };
177
+
178
+ const mappedState = mapFeatureFlagsEnvToState(config, userId);
179
+
180
+ expect(mappedState.isAgentEditable).toBe(false);
181
+ expect(mappedState.showCreateSession).toBe(false);
182
+ expect(mappedState.showDalle).toBe(true);
183
+ });
184
+
185
+ it('should return false for array flags when no user ID provided', () => {
186
+ const config = {
187
+ edit_agent: ['user-123', 'user-456'],
188
+ create_session: true,
55
189
  };
56
190
 
57
191
  const mappedState = mapFeatureFlagsEnvToState(config);
58
192
 
59
- expect(mappedState).toEqual(expectedState);
193
+ expect(mappedState.isAgentEditable).toBe(false);
194
+ expect(mappedState.showCreateSession).toBe(true);
195
+ });
196
+
197
+ it('should handle mixed boolean and array values correctly', () => {
198
+ const userId = 'user-123';
199
+ const config = {
200
+ edit_agent: ['user-123'],
201
+ create_session: true,
202
+ dalle: false,
203
+ ai_image: ['user-456'],
204
+ knowledge_base: ['user-123', 'user-789'],
205
+ rag_eval: true,
206
+ };
207
+
208
+ const mappedState = mapFeatureFlagsEnvToState(config, userId);
209
+
210
+ expect(mappedState.isAgentEditable).toBe(true);
211
+ expect(mappedState.showCreateSession).toBe(true);
212
+ expect(mappedState.showDalle).toBe(false);
213
+ expect(mappedState.showAiImage).toBe(false);
214
+ expect(mappedState.enableKnowledgeBase).toBe(true);
215
+ expect(mappedState.enableRAGEval).toBe(true);
60
216
  });
61
217
  });