@lobehub/chat 1.84.1 → 1.84.3
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/.env.desktop +1 -0
- package/CHANGELOG.md +52 -0
- package/apps/desktop/electron-builder.js +8 -4
- package/apps/desktop/package.json +1 -0
- package/apps/desktop/src/main/index.ts +3 -0
- package/changelog/v1.json +14 -0
- package/locales/ar/models.json +6 -6
- package/locales/ar/plugin.json +10 -1
- package/locales/bg-BG/models.json +6 -6
- package/locales/bg-BG/plugin.json +10 -1
- package/locales/de-DE/models.json +6 -6
- package/locales/de-DE/plugin.json +10 -1
- package/locales/en-US/models.json +6 -6
- package/locales/en-US/plugin.json +10 -1
- package/locales/es-ES/models.json +6 -6
- package/locales/es-ES/plugin.json +10 -1
- package/locales/fa-IR/models.json +6 -6
- package/locales/fa-IR/plugin.json +10 -1
- package/locales/fr-FR/models.json +6 -6
- package/locales/fr-FR/plugin.json +10 -1
- package/locales/it-IT/models.json +6 -6
- package/locales/it-IT/plugin.json +10 -1
- package/locales/ja-JP/models.json +6 -6
- package/locales/ja-JP/plugin.json +10 -1
- package/locales/ko-KR/models.json +6 -6
- package/locales/ko-KR/plugin.json +10 -1
- package/locales/nl-NL/models.json +6 -6
- package/locales/nl-NL/plugin.json +10 -1
- package/locales/pl-PL/models.json +6 -6
- package/locales/pl-PL/plugin.json +10 -1
- package/locales/pt-BR/models.json +6 -6
- package/locales/pt-BR/plugin.json +10 -1
- package/locales/ru-RU/models.json +6 -6
- package/locales/ru-RU/plugin.json +10 -1
- package/locales/tr-TR/models.json +6 -6
- package/locales/tr-TR/plugin.json +10 -1
- package/locales/vi-VN/models.json +6 -6
- package/locales/vi-VN/plugin.json +10 -1
- package/locales/zh-CN/models.json +6 -6
- package/locales/zh-CN/plugin.json +10 -1
- package/locales/zh-TW/models.json +6 -6
- package/locales/zh-TW/plugin.json +10 -1
- package/package.json +2 -2
- package/src/app/[variants]/(main)/_layout/Desktop/SideBar/index.tsx +15 -5
- package/src/app/[variants]/(main)/chat/(workspace)/@conversation/features/ChatInput/Mobile/InputArea/index.tsx +3 -2
- package/src/app/[variants]/(main)/chat/_layout/Desktop/RegisterHotkeys.tsx +6 -3
- package/src/app/[variants]/(main)/chat/settings/page.tsx +4 -33
- package/src/app/[variants]/(main)/settings/_layout/Desktop/Header.tsx +7 -2
- package/src/app/[variants]/(main)/settings/_layout/Mobile/Header.tsx +9 -1
- package/src/app/[variants]/(main)/settings/agent/_layout/Desktop.tsx +1 -1
- package/src/app/[variants]/(main)/settings/agent/_layout/Mobile.tsx +1 -8
- package/src/app/[variants]/(main)/settings/agent/index.tsx +34 -14
- package/src/app/[variants]/(main)/settings/common/features/Common.tsx +1 -0
- package/src/app/[variants]/(main)/settings/provider/features/ModelList/index.tsx +11 -1
- package/src/app/[variants]/(main)/settings/storage/Advanced.tsx +3 -0
- package/src/features/ChatInput/ActionBar/Params/ParamsControls.tsx +17 -7
- package/src/features/ModelSwitchPanel/index.tsx +6 -0
- package/src/features/PluginDevModal/MCPManifestForm/EnvEditor.tsx +227 -0
- package/src/features/PluginDevModal/MCPManifestForm/index.tsx +14 -0
- package/src/hooks/useHotkeys/chatScope.ts +0 -2
- package/src/hooks/useHotkeys/filesScope.ts +0 -2
- package/src/libs/agent-runtime/openai/index.ts +11 -0
- package/src/libs/mcp/client.ts +9 -1
- package/src/libs/mcp/types.ts +1 -0
- package/src/locales/default/plugin.ts +10 -1
- package/src/server/globalConfig/index.ts +3 -0
@@ -7,22 +7,20 @@ import { memo, useState } from 'react';
|
|
7
7
|
import { useTranslation } from 'react-i18next';
|
8
8
|
|
9
9
|
import PageTitle from '@/components/PageTitle';
|
10
|
-
import {
|
10
|
+
import { useCategory } from '@/features/AgentSetting/AgentCategory/useCategory';
|
11
|
+
import AgentSettings from '@/features/AgentSetting/AgentSettings';
|
11
12
|
import { useInitAgentConfig } from '@/hooks/useInitAgentConfig';
|
12
13
|
import { useAgentStore } from '@/store/agent';
|
13
14
|
import { agentSelectors } from '@/store/agent/selectors';
|
14
15
|
import { ChatSettingsTabs } from '@/store/global/initialState';
|
15
|
-
import { featureFlagsSelectors, useServerConfigStore } from '@/store/serverConfig';
|
16
16
|
import { useSessionStore } from '@/store/session';
|
17
17
|
import { sessionMetaSelectors } from '@/store/session/selectors';
|
18
18
|
|
19
|
-
import AgentSettings from '../../../../../features/AgentSetting/AgentSettings';
|
20
|
-
|
21
19
|
const EditPage = memo(() => {
|
22
20
|
const { t } = useTranslation('setting');
|
23
21
|
const [tab, setTab] = useState(ChatSettingsTabs.Prompt);
|
24
22
|
const theme = useTheme();
|
25
|
-
|
23
|
+
const cateItems = useCategory();
|
26
24
|
const [id, updateAgentMeta, title] = useSessionStore((s) => [
|
27
25
|
s.activeId,
|
28
26
|
s.updateSessionMeta,
|
@@ -36,39 +34,12 @@ const EditPage = memo(() => {
|
|
36
34
|
|
37
35
|
const { isLoading } = useInitAgentConfig();
|
38
36
|
|
39
|
-
const { enablePlugins } = useServerConfigStore(featureFlagsSelectors);
|
40
|
-
|
41
37
|
return (
|
42
38
|
<>
|
43
39
|
<PageTitle title={t('header.sessionWithName', { name: title })} />
|
44
40
|
<Tabs
|
45
41
|
compact
|
46
|
-
items={
|
47
|
-
{
|
48
|
-
key: ChatSettingsTabs.Prompt,
|
49
|
-
label: t('settingAgent.prompt.title'),
|
50
|
-
},
|
51
|
-
(id !== INBOX_SESSION_ID && {
|
52
|
-
key: ChatSettingsTabs.Meta,
|
53
|
-
label: t('settingAgent.title'),
|
54
|
-
}) as any,
|
55
|
-
{
|
56
|
-
key: ChatSettingsTabs.Chat,
|
57
|
-
label: t('settingChat.title'),
|
58
|
-
},
|
59
|
-
{
|
60
|
-
key: ChatSettingsTabs.Modal,
|
61
|
-
label: t('settingModel.title'),
|
62
|
-
},
|
63
|
-
{
|
64
|
-
key: ChatSettingsTabs.TTS,
|
65
|
-
label: t('settingTTS.title'),
|
66
|
-
},
|
67
|
-
(enablePlugins && {
|
68
|
-
key: ChatSettingsTabs.Plugin,
|
69
|
-
label: t('settingPlugin.title'),
|
70
|
-
}) as any,
|
71
|
-
]}
|
42
|
+
items={cateItems as any}
|
72
43
|
onChange={(value) => setTab(value as ChatSettingsTabs)}
|
73
44
|
style={{
|
74
45
|
borderBottom: `1px solid ${theme.colorBorderSecondary}`,
|
@@ -15,7 +15,7 @@ const useStyles = createStyles(({ token, css }) => ({
|
|
15
15
|
position: relative;
|
16
16
|
flex: none;
|
17
17
|
height: 54px;
|
18
|
-
background: ${token.
|
18
|
+
background: ${token.colorBgLayout};
|
19
19
|
`,
|
20
20
|
title: css`
|
21
21
|
font-size: 18px;
|
@@ -52,6 +52,11 @@ const Header = memo<HeaderProps>(({ children, getContainer, title }) => {
|
|
52
52
|
}
|
53
53
|
/>
|
54
54
|
}
|
55
|
+
styles={{
|
56
|
+
left: {
|
57
|
+
padding: 0,
|
58
|
+
},
|
59
|
+
}}
|
55
60
|
/>
|
56
61
|
<Drawer
|
57
62
|
getContainer={getContainer}
|
@@ -61,7 +66,7 @@ const Header = memo<HeaderProps>(({ children, getContainer, title }) => {
|
|
61
66
|
placement={'left'}
|
62
67
|
rootStyle={{ position: 'absolute' }}
|
63
68
|
style={{
|
64
|
-
background: theme.
|
69
|
+
background: theme.colorBgLayout,
|
65
70
|
borderRight: `1px solid ${theme.colorSplit}`,
|
66
71
|
}}
|
67
72
|
styles={{
|
@@ -2,12 +2,14 @@
|
|
2
2
|
|
3
3
|
import { Tag } from '@lobehub/ui';
|
4
4
|
import { ChatHeader } from '@lobehub/ui/mobile';
|
5
|
+
import { usePathname } from 'next/navigation';
|
5
6
|
import { memo } from 'react';
|
6
7
|
import { useTranslation } from 'react-i18next';
|
7
8
|
import { Flexbox } from 'react-layout-kit';
|
8
9
|
|
9
10
|
import { enableAuth } from '@/const/auth';
|
10
11
|
import { useActiveSettingsKey } from '@/hooks/useActiveTabKey';
|
12
|
+
import { useProviderName } from '@/hooks/useProviderName';
|
11
13
|
import { useQueryRoute } from '@/hooks/useQueryRoute';
|
12
14
|
import { useShowMobileWorkspace } from '@/hooks/useShowMobileWorkspace';
|
13
15
|
import { SettingsTabs } from '@/store/global/initialState';
|
@@ -21,6 +23,9 @@ const Header = memo(() => {
|
|
21
23
|
const showMobileWorkspace = useShowMobileWorkspace();
|
22
24
|
const activeSettingsKey = useActiveSettingsKey();
|
23
25
|
const isSessionActive = useSessionStore((s) => !!s.activeId);
|
26
|
+
const pathname = usePathname();
|
27
|
+
const isProvider = pathname.includes('/settings/provider/');
|
28
|
+
const providerName = useProviderName(activeSettingsKey);
|
24
29
|
|
25
30
|
const handleBackClick = () => {
|
26
31
|
if (isSessionActive && showMobileWorkspace) {
|
@@ -29,13 +34,16 @@ const Header = memo(() => {
|
|
29
34
|
router.push(enableAuth ? '/me/settings' : '/me');
|
30
35
|
}
|
31
36
|
};
|
37
|
+
|
32
38
|
return (
|
33
39
|
<ChatHeader
|
34
40
|
center={
|
35
41
|
<ChatHeader.Title
|
36
42
|
title={
|
37
43
|
<Flexbox align={'center'} gap={8} horizontal>
|
38
|
-
<span style={{ lineHeight: 1.2 }}>
|
44
|
+
<span style={{ lineHeight: 1.2 }}>
|
45
|
+
{isProvider ? providerName : t(`tab.${activeSettingsKey}`)}
|
46
|
+
</span>
|
39
47
|
{activeSettingsKey === SettingsTabs.Sync && (
|
40
48
|
<Tag bordered={false} color={'warning'}>
|
41
49
|
{t('tab.experiment')}
|
@@ -10,7 +10,7 @@ const Layout = ({ children }: PropsWithChildren) => {
|
|
10
10
|
return (
|
11
11
|
<>
|
12
12
|
<NProgress />
|
13
|
-
<Flexbox horizontal width={'100%'}>
|
13
|
+
<Flexbox height={'100%'} horizontal width={'100%'}>
|
14
14
|
<AgentMenu />
|
15
15
|
<SettingContainer>{children}</SettingContainer>
|
16
16
|
</Flexbox>
|
@@ -1,14 +1,7 @@
|
|
1
|
-
'use client';
|
2
|
-
|
3
|
-
import { usePathname } from 'next/navigation';
|
4
1
|
import { PropsWithChildren } from 'react';
|
5
2
|
|
6
|
-
import ProviderMenu from '../AgentMenu';
|
7
|
-
|
8
3
|
const Layout = ({ children }: PropsWithChildren) => {
|
9
|
-
|
10
|
-
|
11
|
-
return pathname === '/settings/agent' ? <ProviderMenu mobile /> : children;
|
4
|
+
return children;
|
12
5
|
};
|
13
6
|
|
14
7
|
export default Layout;
|
@@ -1,38 +1,58 @@
|
|
1
1
|
'use client';
|
2
2
|
|
3
|
+
import { Tabs } from '@lobehub/ui';
|
4
|
+
import { useTheme } from 'antd-style';
|
3
5
|
import isEqual from 'fast-deep-equal';
|
4
6
|
import { useQueryState } from 'nuqs';
|
5
7
|
import { memo } from 'react';
|
6
8
|
|
7
9
|
import { INBOX_SESSION_ID } from '@/const/session';
|
8
10
|
import { AgentSettings } from '@/features/AgentSetting';
|
11
|
+
import { useCategory } from '@/features/AgentSetting/AgentCategory/useCategory';
|
9
12
|
import { ChatSettingsTabs } from '@/store/global/initialState';
|
13
|
+
import { useServerConfigStore } from '@/store/serverConfig';
|
10
14
|
import { useUserStore } from '@/store/user';
|
11
15
|
import { settingsSelectors } from '@/store/user/selectors';
|
12
16
|
|
13
17
|
const Page = memo(() => {
|
14
|
-
const
|
18
|
+
const cateItems = useCategory();
|
19
|
+
const [tab, setTab] = useQueryState('tab', {
|
15
20
|
defaultValue: ChatSettingsTabs.Prompt,
|
16
21
|
});
|
17
22
|
const config = useUserStore(settingsSelectors.defaultAgentConfig, isEqual);
|
18
23
|
const meta = useUserStore(settingsSelectors.defaultAgentMeta, isEqual);
|
19
24
|
const [updateAgent] = useUserStore((s) => [s.updateDefaultAgent]);
|
20
25
|
const isUserStateInit = useUserStore((s) => s.isUserStateInit);
|
26
|
+
const theme = useTheme();
|
27
|
+
const mobile = useServerConfigStore((s) => s.isMobile);
|
21
28
|
|
22
29
|
return (
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
<>
|
31
|
+
{mobile && (
|
32
|
+
<Tabs
|
33
|
+
activeKey={tab}
|
34
|
+
compact
|
35
|
+
items={cateItems as any}
|
36
|
+
onChange={(value) => setTab(value as ChatSettingsTabs)}
|
37
|
+
style={{
|
38
|
+
borderBottom: `1px solid ${theme.colorBorderSecondary}`,
|
39
|
+
}}
|
40
|
+
/>
|
41
|
+
)}
|
42
|
+
<AgentSettings
|
43
|
+
config={config}
|
44
|
+
id={INBOX_SESSION_ID}
|
45
|
+
loading={!isUserStateInit}
|
46
|
+
meta={meta}
|
47
|
+
onConfigChange={(config) => {
|
48
|
+
updateAgent({ config });
|
49
|
+
}}
|
50
|
+
onMetaChange={(meta) => {
|
51
|
+
updateAgent({ meta });
|
52
|
+
}}
|
53
|
+
tab={tab as ChatSettingsTabs}
|
54
|
+
/>
|
55
|
+
</>
|
36
56
|
);
|
37
57
|
});
|
38
58
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
'use client';
|
2
2
|
|
3
|
+
import { useTheme } from 'antd-style';
|
3
4
|
import { Suspense, memo } from 'react';
|
4
5
|
import { Flexbox } from 'react-layout-kit';
|
5
6
|
|
@@ -48,12 +49,21 @@ interface ModelListProps extends ProviderSettingsContextValue {
|
|
48
49
|
const ModelList = memo<ModelListProps>(
|
49
50
|
({ id, showModelFetcher, sdkType, showAddNewModel, showDeployName, modelEditable = true }) => {
|
50
51
|
const mobile = useIsMobile();
|
52
|
+
const theme = useTheme();
|
51
53
|
|
52
54
|
return (
|
53
55
|
<ProviderSettingsContext
|
54
56
|
value={{ modelEditable, sdkType, showAddNewModel, showDeployName, showModelFetcher }}
|
55
57
|
>
|
56
|
-
<Flexbox
|
58
|
+
<Flexbox
|
59
|
+
gap={16}
|
60
|
+
paddingInline={mobile ? 12 : 0}
|
61
|
+
style={{
|
62
|
+
background: mobile ? theme.colorBgContainer : undefined,
|
63
|
+
paddingBottom: 16,
|
64
|
+
paddingTop: 8,
|
65
|
+
}}
|
66
|
+
>
|
57
67
|
<ModelTitle
|
58
68
|
provider={id}
|
59
69
|
showAddNewModel={showAddNewModel}
|
@@ -64,6 +64,7 @@ const AdvancedActions = () => {
|
|
64
64
|
</DataImporter>
|
65
65
|
),
|
66
66
|
label: t('storage.actions.import.title'),
|
67
|
+
layout: 'horizontal',
|
67
68
|
minWidth: undefined,
|
68
69
|
},
|
69
70
|
{
|
@@ -78,6 +79,7 @@ const AdvancedActions = () => {
|
|
78
79
|
</Button>
|
79
80
|
),
|
80
81
|
label: t('storage.actions.export.title'),
|
82
|
+
layout: 'horizontal',
|
81
83
|
minWidth: undefined,
|
82
84
|
},
|
83
85
|
{
|
@@ -88,6 +90,7 @@ const AdvancedActions = () => {
|
|
88
90
|
),
|
89
91
|
desc: t('danger.clear.desc'),
|
90
92
|
label: t('danger.clear.title'),
|
93
|
+
layout: 'horizontal',
|
91
94
|
minWidth: undefined,
|
92
95
|
},
|
93
96
|
],
|
@@ -14,21 +14,21 @@ import {
|
|
14
14
|
} from '@/features/ModelParamsControl';
|
15
15
|
import { useAgentStore } from '@/store/agent';
|
16
16
|
import { agentSelectors } from '@/store/agent/selectors';
|
17
|
+
import { useServerConfigStore } from '@/store/serverConfig';
|
17
18
|
|
18
19
|
interface ParamsControlsProps {
|
19
20
|
setUpdating: (updating: boolean) => void;
|
20
21
|
}
|
21
22
|
const ParamsControls = memo<ParamsControlsProps>(({ setUpdating }) => {
|
22
23
|
const { t } = useTranslation('setting');
|
23
|
-
|
24
|
+
const mobile = useServerConfigStore((s) => s.isMobile);
|
24
25
|
const updateAgentConfig = useAgentStore((s) => s.updateAgentConfig);
|
25
26
|
|
26
27
|
const config = useAgentStore(agentSelectors.currentAgentConfig, isEqual);
|
27
28
|
|
28
|
-
|
29
|
+
let items: FormItemProps[] = [
|
29
30
|
{
|
30
31
|
children: <Temperature />,
|
31
|
-
desc: <Tag>temperature</Tag>,
|
32
32
|
label: (
|
33
33
|
<Flexbox align={'center'} gap={8} horizontal justify={'space-between'}>
|
34
34
|
{t('settingModel.temperature.title')}
|
@@ -36,10 +36,10 @@ const ParamsControls = memo<ParamsControlsProps>(({ setUpdating }) => {
|
|
36
36
|
</Flexbox>
|
37
37
|
),
|
38
38
|
name: ['params', 'temperature'],
|
39
|
+
tag: 'temperature',
|
39
40
|
},
|
40
41
|
{
|
41
42
|
children: <TopP />,
|
42
|
-
desc: <Tag>top_p</Tag>,
|
43
43
|
label: (
|
44
44
|
<Flexbox gap={8} horizontal>
|
45
45
|
{t('settingModel.topP.title')}
|
@@ -47,10 +47,10 @@ const ParamsControls = memo<ParamsControlsProps>(({ setUpdating }) => {
|
|
47
47
|
</Flexbox>
|
48
48
|
),
|
49
49
|
name: ['params', 'top_p'],
|
50
|
+
tag: 'top_p',
|
50
51
|
},
|
51
52
|
{
|
52
53
|
children: <PresencePenalty />,
|
53
|
-
desc: <Tag>presence_penalty</Tag>,
|
54
54
|
label: (
|
55
55
|
<Flexbox gap={8} horizontal>
|
56
56
|
{t('settingModel.presencePenalty.title')}
|
@@ -58,10 +58,10 @@ const ParamsControls = memo<ParamsControlsProps>(({ setUpdating }) => {
|
|
58
58
|
</Flexbox>
|
59
59
|
),
|
60
60
|
name: ['params', 'presence_penalty'],
|
61
|
+
tag: 'presence_penalty',
|
61
62
|
},
|
62
63
|
{
|
63
64
|
children: <FrequencyPenalty />,
|
64
|
-
desc: <Tag>frequency_penalty</Tag>,
|
65
65
|
label: (
|
66
66
|
<Flexbox gap={8} horizontal>
|
67
67
|
{t('settingModel.frequencyPenalty.title')}
|
@@ -69,6 +69,7 @@ const ParamsControls = memo<ParamsControlsProps>(({ setUpdating }) => {
|
|
69
69
|
</Flexbox>
|
70
70
|
),
|
71
71
|
name: ['params', 'frequency_penalty'],
|
72
|
+
tag: 'frequency_penalty',
|
72
73
|
},
|
73
74
|
];
|
74
75
|
|
@@ -76,7 +77,9 @@ const ParamsControls = memo<ParamsControlsProps>(({ setUpdating }) => {
|
|
76
77
|
<Form
|
77
78
|
initialValues={config}
|
78
79
|
itemMinWidth={200}
|
79
|
-
items={
|
80
|
+
items={
|
81
|
+
mobile ? items : items.map(({ tag, ...item }) => ({ ...item, desc: <Tag>{tag}</Tag> }))
|
82
|
+
}
|
80
83
|
itemsType={'flat'}
|
81
84
|
onValuesChange={debounce(async (values) => {
|
82
85
|
setUpdating(true);
|
@@ -84,6 +87,13 @@ const ParamsControls = memo<ParamsControlsProps>(({ setUpdating }) => {
|
|
84
87
|
setUpdating(false);
|
85
88
|
}, 500)}
|
86
89
|
style={{ fontSize: 12 }}
|
90
|
+
styles={{
|
91
|
+
group: {
|
92
|
+
background: 'transparent',
|
93
|
+
paddingBottom: mobile ? 16 : 0,
|
94
|
+
paddingInline: 0,
|
95
|
+
},
|
96
|
+
}}
|
87
97
|
variant={'borderless'}
|
88
98
|
/>
|
89
99
|
);
|
@@ -135,6 +135,12 @@ const ModelSwitchPanel = memo<PropsWithChildren>(({ children }) => {
|
|
135
135
|
activeKey: menuKey(provider, model),
|
136
136
|
className: styles.menu,
|
137
137
|
items,
|
138
|
+
// 不加限高就会导致面板超长,顶部的内容会被隐藏
|
139
|
+
// https://github.com/user-attachments/assets/9c043c47-42c5-46ef-b5c1-bee89376f042
|
140
|
+
style: {
|
141
|
+
maxHeight: 500,
|
142
|
+
overflowY: 'scroll',
|
143
|
+
},
|
138
144
|
}}
|
139
145
|
placement={'topLeft'}
|
140
146
|
>
|
@@ -0,0 +1,227 @@
|
|
1
|
+
import { ActionIcon, Icon } from '@lobehub/ui';
|
2
|
+
import { Button } from 'antd';
|
3
|
+
import { createStyles } from 'antd-style';
|
4
|
+
import fastDeepEqual from 'fast-deep-equal';
|
5
|
+
import { LucidePlus, LucideTrash } from 'lucide-react';
|
6
|
+
import { memo, useEffect, useRef, useState } from 'react';
|
7
|
+
import { useTranslation } from 'react-i18next';
|
8
|
+
import { Flexbox } from 'react-layout-kit';
|
9
|
+
import { v4 as uuidv4 } from 'uuid';
|
10
|
+
|
11
|
+
import { FormInput } from '@/components/FormInput';
|
12
|
+
|
13
|
+
const useStyles = createStyles(({ css, token }) => ({
|
14
|
+
container: css`
|
15
|
+
position: relative;
|
16
|
+
|
17
|
+
width: 100%;
|
18
|
+
padding: 12px;
|
19
|
+
border: 1px solid ${token.colorBorderSecondary};
|
20
|
+
border-radius: ${token.borderRadiusLG}px;
|
21
|
+
`,
|
22
|
+
input: css`
|
23
|
+
font-family: ${token.fontFamilyCode};
|
24
|
+
font-size: 12px;
|
25
|
+
`,
|
26
|
+
row: css`
|
27
|
+
margin-block-end: 8px;
|
28
|
+
|
29
|
+
&:last-child {
|
30
|
+
margin-block-end: 0;
|
31
|
+
}
|
32
|
+
`,
|
33
|
+
title: css`
|
34
|
+
margin-block-end: 8px;
|
35
|
+
color: ${token.colorTextTertiary};
|
36
|
+
`,
|
37
|
+
}));
|
38
|
+
|
39
|
+
interface KeyValueItem {
|
40
|
+
id: string;
|
41
|
+
key: string;
|
42
|
+
value: string;
|
43
|
+
}
|
44
|
+
|
45
|
+
export interface EnvEditorProps {
|
46
|
+
onChange?: (value: Record<string, string>) => void;
|
47
|
+
value?: Record<string, string>;
|
48
|
+
}
|
49
|
+
|
50
|
+
const recordToLocalList = (
|
51
|
+
record: Record<string, string> | undefined | null = {},
|
52
|
+
): KeyValueItem[] =>
|
53
|
+
Object.entries(record || {}).map(([key, val]) => ({
|
54
|
+
id: uuidv4(),
|
55
|
+
key,
|
56
|
+
value: typeof val === 'string' ? val : '',
|
57
|
+
}));
|
58
|
+
|
59
|
+
const localListToRecord = (
|
60
|
+
list: KeyValueItem[] | undefined | null = [],
|
61
|
+
): Record<string, string> => {
|
62
|
+
const record: Record<string, string> = {};
|
63
|
+
const keys = new Set<string>();
|
64
|
+
(list || [])
|
65
|
+
.slice()
|
66
|
+
.reverse()
|
67
|
+
.forEach((item) => {
|
68
|
+
const trimmedKey = item.key.trim();
|
69
|
+
if (trimmedKey && !keys.has(trimmedKey)) {
|
70
|
+
record[trimmedKey] = typeof item.value === 'string' ? item.value : '';
|
71
|
+
keys.add(trimmedKey);
|
72
|
+
}
|
73
|
+
});
|
74
|
+
return Object.keys(record)
|
75
|
+
.reverse()
|
76
|
+
.reduce(
|
77
|
+
(acc, key) => {
|
78
|
+
acc[key] = record[key];
|
79
|
+
return acc;
|
80
|
+
},
|
81
|
+
{} as Record<string, string>,
|
82
|
+
);
|
83
|
+
};
|
84
|
+
|
85
|
+
const EnvEditor = memo<EnvEditorProps>(({ value, onChange }) => {
|
86
|
+
const { styles } = useStyles();
|
87
|
+
const { t } = useTranslation(['plugin', 'common']);
|
88
|
+
const [items, setItems] = useState<KeyValueItem[]>(() => recordToLocalList(value));
|
89
|
+
const prevValueRef = useRef<Record<string, string> | undefined>(undefined);
|
90
|
+
|
91
|
+
useEffect(() => {
|
92
|
+
const externalRecord = value || {};
|
93
|
+
if (!fastDeepEqual(externalRecord, prevValueRef.current)) {
|
94
|
+
setItems(recordToLocalList(externalRecord));
|
95
|
+
prevValueRef.current = externalRecord;
|
96
|
+
}
|
97
|
+
}, [value]);
|
98
|
+
|
99
|
+
const triggerChange = (newItems: KeyValueItem[]) => {
|
100
|
+
const keysCount: Record<string, number> = {};
|
101
|
+
newItems.forEach((item) => {
|
102
|
+
const trimmedKey = item.key.trim();
|
103
|
+
if (trimmedKey) {
|
104
|
+
keysCount[trimmedKey] = (keysCount[trimmedKey] || 0) + 1;
|
105
|
+
}
|
106
|
+
});
|
107
|
+
setItems(
|
108
|
+
newItems.map((item) => ({
|
109
|
+
...item,
|
110
|
+
})),
|
111
|
+
);
|
112
|
+
onChange?.(localListToRecord(newItems));
|
113
|
+
};
|
114
|
+
|
115
|
+
const handleAdd = () => {
|
116
|
+
const newItems = [...items, { id: uuidv4(), key: '', value: '' }];
|
117
|
+
triggerChange(newItems);
|
118
|
+
};
|
119
|
+
|
120
|
+
const handleRemove = (id: string) => {
|
121
|
+
const newItems = items.filter((item) => item.id !== id);
|
122
|
+
triggerChange(newItems);
|
123
|
+
};
|
124
|
+
|
125
|
+
const handleKeyChange = (id: string, newKey: string) => {
|
126
|
+
const newItems = items.map((item) => (item.id === id ? { ...item, key: newKey } : item));
|
127
|
+
triggerChange(newItems);
|
128
|
+
};
|
129
|
+
|
130
|
+
const handleValueChange = (id: string, newValue: string) => {
|
131
|
+
const newItems = items.map((item) => (item.id === id ? { ...item, value: newValue } : item));
|
132
|
+
triggerChange(newItems);
|
133
|
+
};
|
134
|
+
|
135
|
+
const getDuplicateKeys = (currentItems: KeyValueItem[]): Set<string> => {
|
136
|
+
const keys = new Set<string>();
|
137
|
+
const duplicates = new Set<string>();
|
138
|
+
currentItems.forEach((item) => {
|
139
|
+
const trimmedKey = item.key.trim();
|
140
|
+
if (trimmedKey) {
|
141
|
+
if (keys.has(trimmedKey)) {
|
142
|
+
duplicates.add(trimmedKey);
|
143
|
+
} else {
|
144
|
+
keys.add(trimmedKey);
|
145
|
+
}
|
146
|
+
}
|
147
|
+
});
|
148
|
+
return duplicates;
|
149
|
+
};
|
150
|
+
const duplicateKeys = getDuplicateKeys(items);
|
151
|
+
|
152
|
+
return (
|
153
|
+
<div className={styles.container}>
|
154
|
+
<Flexbox className={styles.title} gap={8} horizontal>
|
155
|
+
<Flexbox flex={1}>key</Flexbox>
|
156
|
+
<Flexbox flex={2}>value</Flexbox>
|
157
|
+
<Flexbox style={{ width: 30 }} />
|
158
|
+
</Flexbox>
|
159
|
+
<Flexbox width={'100%'}>
|
160
|
+
{items.map((item) => {
|
161
|
+
const isDuplicate = item.key.trim() && duplicateKeys.has(item.key.trim());
|
162
|
+
return (
|
163
|
+
<Flexbox
|
164
|
+
align="flex-start"
|
165
|
+
className={styles.row}
|
166
|
+
gap={8}
|
167
|
+
horizontal
|
168
|
+
key={item.id}
|
169
|
+
width={'100%'}
|
170
|
+
>
|
171
|
+
<Flexbox flex={1} style={{ position: 'relative' }}>
|
172
|
+
<FormInput
|
173
|
+
className={styles.input}
|
174
|
+
onChange={(e) => handleKeyChange(item.id, e)}
|
175
|
+
placeholder={'key'}
|
176
|
+
status={isDuplicate ? 'error' : undefined}
|
177
|
+
value={item.key}
|
178
|
+
variant={'filled'}
|
179
|
+
/>
|
180
|
+
{isDuplicate && (
|
181
|
+
<div
|
182
|
+
style={{
|
183
|
+
bottom: '-16px',
|
184
|
+
color: 'red',
|
185
|
+
fontSize: '12px',
|
186
|
+
position: 'absolute',
|
187
|
+
}}
|
188
|
+
>
|
189
|
+
{t('dev.mcp.env.duplicateKeyError')}
|
190
|
+
</div>
|
191
|
+
)}
|
192
|
+
</Flexbox>
|
193
|
+
<Flexbox flex={2}>
|
194
|
+
<FormInput
|
195
|
+
className={styles.input}
|
196
|
+
onChange={(value) => handleValueChange(item.id, value)}
|
197
|
+
placeholder={'value'}
|
198
|
+
value={item.value}
|
199
|
+
variant={'filled'}
|
200
|
+
/>
|
201
|
+
</Flexbox>
|
202
|
+
<ActionIcon
|
203
|
+
icon={LucideTrash}
|
204
|
+
onClick={() => handleRemove(item.id)}
|
205
|
+
size={'small'}
|
206
|
+
style={{ marginTop: 4 }}
|
207
|
+
title={t('delete', { ns: 'common' })}
|
208
|
+
/>
|
209
|
+
</Flexbox>
|
210
|
+
);
|
211
|
+
})}
|
212
|
+
<Button
|
213
|
+
block
|
214
|
+
icon={<Icon icon={LucidePlus} />}
|
215
|
+
onClick={handleAdd}
|
216
|
+
size={'small'}
|
217
|
+
style={{ marginTop: items.length > 0 ? 16 : 8 }}
|
218
|
+
type="dashed"
|
219
|
+
>
|
220
|
+
{t('dev.mcp.env.add')}
|
221
|
+
</Button>
|
222
|
+
</Flexbox>
|
223
|
+
</div>
|
224
|
+
);
|
225
|
+
});
|
226
|
+
|
227
|
+
export default EnvEditor;
|