@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.
- package/CHANGELOG.md +58 -0
- package/apps/desktop/src/main/appBrowsers.ts +51 -0
- package/apps/desktop/src/main/controllers/BrowserWindowsCtr.ts +72 -1
- package/apps/desktop/src/main/core/browser/BrowserManager.ts +88 -18
- package/changelog/v1.json +21 -0
- package/package.json +1 -1
- package/packages/database/src/models/__tests__/aiModel.test.ts +3 -0
- package/packages/database/src/models/aiModel.ts +18 -2
- package/packages/electron-client-ipc/src/events/windows.ts +39 -0
- package/packages/model-bank/src/aiModels/google.ts +64 -2
- package/packages/model-bank/src/types/aiModel.ts +13 -9
- package/packages/model-runtime/src/providers/google/createImage.ts +13 -4
- package/src/app/[variants]/(main)/_layout/Desktop/DesktopLayoutContainer.tsx +4 -2
- package/src/app/[variants]/(main)/_layout/Desktop/SideBar/index.tsx +3 -1
- package/src/app/[variants]/(main)/_layout/Desktop/index.tsx +3 -1
- package/src/app/[variants]/(main)/chat/(workspace)/@topic/features/TopicListContent/TopicItem/TopicContent.tsx +25 -1
- package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +19 -2
- package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx +27 -1
- package/src/app/[variants]/(main)/chat/_layout/Desktop/SessionPanel.tsx +11 -1
- package/src/app/[variants]/(main)/chat/features/TogglePanelButton.tsx +6 -0
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/ModelItem.tsx +6 -3
- package/src/config/featureFlags/index.ts +2 -2
- package/src/config/featureFlags/schema.test.ts +165 -9
- package/src/config/featureFlags/schema.ts +68 -46
- package/src/features/ElectronTitlebar/Connection/index.tsx +0 -1
- package/src/hooks/useIsSingleMode.test.ts +66 -0
- package/src/hooks/useIsSingleMode.ts +29 -0
- package/src/server/featureFlags/index.ts +56 -0
- package/src/server/modules/EdgeConfig/index.ts +43 -4
- package/src/server/routers/lambda/aiModel.test.ts +2 -0
- 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
|
-
|
|
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}>
|
|
@@ -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}
|
package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx
CHANGED
|
@@ -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 (
|
package/src/app/[variants]/(main)/chat/@session/features/SessionListContent/List/Item/index.tsx
CHANGED
|
@@ -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
|
|
20
|
+
it('should validate correct feature flags with user ID arrays', () => {
|
|
22
21
|
const result = FeatureFlagsSchema.safeParse({
|
|
23
|
-
edit_agent: '
|
|
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
|
|
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).
|
|
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
|
});
|