Compare commits

...

1 Commits

Author SHA1 Message Date
arvinxx 4fa6d64df2 add plugin settings 2025-07-29 14:48:46 +08:00
9 changed files with 232 additions and 1 deletions
+1
View File
@@ -535,6 +535,7 @@
"experiment": "Experiment",
"hotkey": "Hotkeys",
"llm": "Language Model",
"plugin": "Plugin Management",
"provider": "AI Service Provider",
"proxy": "Network Proxy",
"storage": "Data Storage",
+1
View File
@@ -535,6 +535,7 @@
"experiment": "实验",
"hotkey": "快捷键",
"llm": "语言模型",
"plugin": "插件管理",
"provider": "AI 服务商",
"proxy": "网络代理",
"storage": "数据存储",
@@ -18,7 +18,7 @@ import { LayoutProps } from '../type';
import Header from './Header';
import SideBar from './SideBar';
const SKIP_PATHS = ['/settings/provider', '/settings/agent'];
const SKIP_PATHS = ['/settings/provider', '/settings/agent', '/settings/plugin'];
const Layout = memo<LayoutProps>(({ children, category }) => {
const ref = useRef<any>(null);
@@ -8,6 +8,7 @@ import {
Info,
KeyboardIcon,
Mic2,
Puzzle,
Settings2,
Sparkles,
} from 'lucide-react';
@@ -115,6 +116,15 @@ export const useCategory = () => {
</Link>
),
},
{
icon: <Icon icon={Puzzle} />,
key: SettingsTabs.Plugin,
label: (
<Link href={'/settings/plugin'} onClick={(e) => e.preventDefault()}>
{t('tab.plugin')}
</Link>
),
},
{
type: 'divider',
},
@@ -0,0 +1,23 @@
import { memo } from 'react';
import McpDetail from '@/features/PluginStore/McpList/Detail';
import PluginDetail from '@/features/PluginStore/PluginList/Detail';
import CustomPluginEmptyState from '@/features/PluginStore/InstalledList/Detail/CustomPluginEmptyState';
interface DetailProps {
identifier: string;
runtimeType?: 'mcp' | 'default';
type?: 'plugin' | 'customPlugin' | 'builtin';
}
const Detail = memo<DetailProps>(({ identifier, type, runtimeType }) => {
if (type === 'customPlugin') return <CustomPluginEmptyState identifier={identifier} />;
if (runtimeType === 'mcp') return <McpDetail identifier={identifier} />;
if (type === 'plugin') return <PluginDetail identifier={identifier} />;
return null;
});
export default Detail;
@@ -0,0 +1,77 @@
import { Empty } from 'antd';
import isEqual from 'fast-deep-equal';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { Center, Flexbox } from 'react-layout-kit';
import { Virtuoso } from 'react-virtuoso';
import { useToolStore } from '@/store/tool';
import { pluginSelectors } from '@/store/tool/selectors';
import { LobeToolType } from '@/types/tool/tool';
import PluginItem from '@/features/PluginStore/InstalledList/List/Item';
interface ListProps {
identifier?: string;
keywords?: string;
setIdentifier?: (props: {
identifier?: string;
runtimeType: 'mcp' | 'default';
type?: LobeToolType;
}) => void;
}
export const List = memo<ListProps>(({ keywords, identifier, setIdentifier }) => {
const { t } = useTranslation('plugin');
const installedPlugins = useToolStore(pluginSelectors.installedPluginMetaList, isEqual);
const filteredPluginList = useMemo(
() =>
installedPlugins.filter((item) =>
[item?.title, item?.description, item.author, ...(item?.tags || [])]
.filter(Boolean)
.join('')
.toLowerCase()
.includes((keywords || '')?.toLowerCase()),
),
[installedPlugins, keywords],
);
const isEmpty = installedPlugins.length === 0;
if (isEmpty)
return (
<Center paddingBlock={40}>
<Empty description={t('store.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
</Center>
);
return (
<Virtuoso
data={filteredPluginList}
itemContent={(_, item) => {
return (
<Flexbox
key={item.identifier}
onClick={() => {
setIdentifier?.({
identifier: item.identifier,
runtimeType: item.runtimeType as any,
type: item.type,
});
}}
paddingBlock={2}
paddingInline={4}
>
<PluginItem active={identifier === item.identifier} {...(item as any)} />
</Flexbox>
);
}}
overscan={400}
style={{ height: '100%', width: '100%' }}
totalCount={filteredPluginList.length}
/>
);
});
export default List;
@@ -0,0 +1,101 @@
'use client';
import { DraggablePanel } from '@lobehub/ui';
import { Empty, Input } from 'antd';
import { useTheme } from 'antd-style';
import { Search } from 'lucide-react';
import { memo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Center, Flexbox } from 'react-layout-kit';
import { useFetchInstalledPlugins } from '@/hooks/useFetchInstalledPlugins';
import { useToolStore } from '@/store/tool';
import { pluginSelectors } from '@/store/tool/selectors';
import { LobeToolType } from '@/types/tool/tool';
import Detail from './components/Detail';
import List from './components/List';
const PluginSettings = memo(() => {
const { t } = useTranslation('plugin');
const ref = useRef<HTMLDivElement>(null);
const theme = useTheme();
const [keywords, setKeywords] = useState<string>('');
const [type, setType] = useState<LobeToolType>();
const [runtimeType, setRuntimeType] = useState<'mcp' | 'default'>();
const [identifier] = useToolStore((s) => [s.activePluginIdentifier]);
const isEmpty = useToolStore((s) => pluginSelectors.installedPluginMetaList(s).length === 0);
useFetchInstalledPlugins();
if (isEmpty)
return (
<Center height={'60vh'} paddingBlock={40}>
<Empty description={t('store.empty')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
</Center>
);
return (
<Flexbox
height={'100vh'}
horizontal
style={{
overflow: 'hidden',
position: 'relative',
}}
width={'100%'}
>
<DraggablePanel maxWidth={1024} minWidth={420} placement={'left'}>
<Flexbox padding={8}>
<Input
allowClear
onChange={(e) => setKeywords(e.target.value)}
placeholder={t('store.search')}
prefix={<Search size={16} />}
style={{ width: '100%' }}
value={keywords}
/>
</Flexbox>
<List
identifier={identifier}
keywords={keywords}
setIdentifier={({ identifier, type, runtimeType }) => {
useToolStore.setState({ activePluginIdentifier: identifier });
setType(type);
setRuntimeType(runtimeType);
ref?.current?.scrollTo({ top: 0 });
}}
/>
</DraggablePanel>
{identifier ? (
<Flexbox
height={'100%'}
padding={16}
ref={ref}
style={{
background: theme.colorBgContainerSecondary,
overflowX: 'hidden',
overflowY: 'auto',
}}
width={'100%'}
>
<Detail identifier={identifier} runtimeType={runtimeType} type={type} />
</Flexbox>
) : (
<Center
height={'100%'}
style={{
background: theme.colorBgContainerSecondary,
}}
width={'100%'}
>
<Empty description={t('store.emptySelectHint')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
</Center>
)}
</Flexbox>
);
});
export default PluginSettings;
@@ -0,0 +1,17 @@
import { metadataModule } from '@/server/metadata';
import { translation } from '@/server/translation';
import { DynamicLayoutProps } from '@/types/next';
import { RouteVariants } from '@/utils/server/routeVariants';
export const generateMetadata = async (props: DynamicLayoutProps) => {
const locale = await RouteVariants.getLocale(props);
const { t } = await translation('setting', locale);
return metadataModule.generate({
description: t('header.desc'),
title: t('tab.plugin'),
url: '/settings/plugin',
});
};
export { default } from './index';
+1
View File
@@ -31,6 +31,7 @@ export enum SettingsTabs {
Common = 'common',
Hotkey = 'hotkey',
LLM = 'llm',
Plugin = 'plugin',
Provider = 'provider',
Proxy = 'proxy',
Storage = 'storage',