Skip to content

Commit

Permalink
Merge pull request #124 from lobehub/feat/plugin-dev
Browse files Browse the repository at this point in the history
✨ feat: 插件开发者模式
  • Loading branch information
arvinxx authored Aug 29, 2023
2 parents e370851 + 902e7ed commit 57b2438
Show file tree
Hide file tree
Showing 22 changed files with 941 additions and 199 deletions.
18 changes: 13 additions & 5 deletions src/components/EmojiPicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,29 @@ import Picker from '@emoji-mart/react';
import { Avatar } from '@lobehub/ui';
import { Popover } from 'antd';
import { memo } from 'react';
import useMergeState from 'use-merge-value';

import { DEFAULT_AVATAR, DEFAULT_BACKGROUND_COLOR } from '@/const/meta';

import { useStyles } from './style';

export interface EmojiPickerProps {
avatar?: string;
backgroundColor?: string;
onChange: (emoji: string) => void;
defaultAvatar?: string;
onChange?: (emoji: string) => void;
value?: string;
}

const EmojiPicker = memo<EmojiPickerProps>(
({ avatar = DEFAULT_AVATAR, backgroundColor = DEFAULT_BACKGROUND_COLOR, onChange }) => {
({ value, defaultAvatar, backgroundColor = DEFAULT_BACKGROUND_COLOR, onChange }) => {
const { styles } = useStyles();

const [ava, setAva] = useMergeState(DEFAULT_AVATAR, {
defaultValue: defaultAvatar,
onChange,
value,
});

return (
<Popover
content={
Expand All @@ -27,7 +35,7 @@ const EmojiPicker = memo<EmojiPickerProps>(
data={data}
i18n={i18n}
locale={'zh'}
onEmojiSelect={(e: any) => onChange(e.native)}
onEmojiSelect={(e: any) => setAva(e.native)}
skinTonePosition={'none'}
theme={'auto'}
/>
Expand All @@ -38,7 +46,7 @@ const EmojiPicker = memo<EmojiPickerProps>(
trigger={'click'}
>
<div className={styles.avatar} style={{ width: 'fit-content' }}>
<Avatar avatar={avatar} background={backgroundColor} size={44} />
<Avatar avatar={ava} background={backgroundColor} size={44} />
</div>
</Popover>
);
Expand Down
2 changes: 1 addition & 1 deletion src/features/AgentSetting/AgentMeta/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ const AgentMeta = memo(() => {
{
children: (
<EmojiPicker
avatar={meta.avatar}
backgroundColor={meta.backgroundColor}
onChange={(avatar) => updateMeta({ avatar })}
value={meta.avatar}
/>
),
label: t('settingAgent.avatar.title'),
Expand Down
72 changes: 72 additions & 0 deletions src/features/AgentSetting/AgentPlugin/LocalPluginItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Button, Switch } from 'antd';
import isEqual from 'fast-deep-equal';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import DevModal from 'src/features/PluginDevModal';

import { pluginSelectors, usePluginStore } from '@/store/plugin';

import { useStore } from '../store';

const MarketList = memo<{ id: string }>(({ id }) => {
const { t } = useTranslation('plugin');

const [showModal, setModal] = useState(false);

const updateConfig = useStore((s) => s.toggleAgentPlugin);
const [plugins, hasPlugin] = useStore((s) => [s.config.plugins || [], !!s.config.plugins]);

const [useFetchPluginList, fetchPluginManifest, dispatchDevPluginList] = usePluginStore((s) => [
s.useFetchPluginList,
s.fetchPluginManifest,
s.dispatchDevPluginList,
]);

const pluginManifestLoading = usePluginStore((s) => s.pluginManifestLoading, isEqual);
const devPlugin = usePluginStore(pluginSelectors.getDevPluginById(id), isEqual);

useFetchPluginList();

return (
<>
<DevModal
onDelete={() => {
dispatchDevPluginList({ id, type: 'deleteItem' });
}}
onOpenChange={setModal}
onSave={(value) => {
dispatchDevPluginList({ id, plugin: value, type: 'updateItem' });
}}
open={showModal}
showDelete
value={devPlugin}
/>

<Flexbox align={'center'} gap={8} horizontal>
<Switch
checked={
// 如果在加载中,说明激活了
pluginManifestLoading[id] || !hasPlugin ? false : plugins.includes(id)
}
loading={pluginManifestLoading[id]}
onChange={(checked) => {
updateConfig(id);
if (checked) {
fetchPluginManifest(id);
}
}}
/>
<Button
onClick={() => {
setModal(true);
}}
>
{t('list.item.local.config')}
</Button>
</Flexbox>
</>
);
});

export default MarketList;
160 changes: 160 additions & 0 deletions src/features/AgentSetting/AgentPlugin/MarketList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { Avatar, Form, Icon, Tooltip } from '@lobehub/ui';
import { Button, Skeleton, Switch, Tag } from 'antd';
import { createStyles } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { LucideBlocks, LucideStore } from 'lucide-react';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';
import DevModal from 'src/features/PluginDevModal';

import { FORM_STYLE } from '@/const/layoutTokens';
import { pluginHelpers, usePluginStore } from '@/store/plugin';

import { useStore } from '../store';
import LocalPluginItem from './LocalPluginItem';

const useStyles = createStyles(({ css }) => ({
avatar: css`
.ant-skeleton-header {
padding-right: 0;
}
`,
label: css`
li {
height: 14px !important;
}
li + li {
margin-block-start: 12px !important;
}
`,
}));

const MarketList = memo(() => {
const { t } = useTranslation('setting');
const { styles } = useStyles();

const [showModal, setModal] = useState(false);

// useEffect(() => {
// setModal(true);
// }, []);

const updateConfig = useStore((s) => s.toggleAgentPlugin);
const [plugins, hasPlugin] = useStore((s) => [s.config.plugins || [], !!s.config.plugins]);

const [useFetchPluginList, fetchPluginManifest, saveToDevList, updateNewDevPlugin] =
usePluginStore((s) => [
s.useFetchPluginList,
s.fetchPluginManifest,
s.saveToDevList,
s.updateNewDevPlugin,
]);
const pluginManifestLoading = usePluginStore((s) => s.pluginManifestLoading, isEqual);
const pluginList = usePluginStore((s) => s.pluginList, isEqual);
const devPluginList = usePluginStore((s) => s.devPluginList, isEqual);

useFetchPluginList();

const loadingItem = {
avatar: (
<Skeleton
active
avatar={{ size: 40 }}
className={styles.avatar}
paragraph={false}
title={false}
/>
),
label: (
<Skeleton
active
avatar={false}
paragraph={{ className: styles.label, style: { marginBottom: 0 } }}
style={{ width: 300 }}
title={false}
/>
),
};

const loadingList = [loadingItem, loadingItem, loadingItem];

const isEmpty = pluginList.length === 0;

const list = pluginList.map(({ identifier, meta }) => ({
avatar: <Avatar avatar={meta.avatar} />,
children: (
<Switch
checked={
// 如果在加载中,说明激活了
pluginManifestLoading[identifier] || !hasPlugin ? false : plugins.includes(identifier)
}
loading={pluginManifestLoading[identifier]}
onChange={(checked) => {
updateConfig(identifier);
if (checked) {
fetchPluginManifest(identifier);
}
}}
/>
),
desc: pluginHelpers.getPluginDesc(meta),
label: pluginHelpers.getPluginTitle(meta),
minWidth: undefined,
tag: identifier,
}));

const devList = devPluginList.map(({ identifier, meta }) => ({
avatar: <Avatar avatar={pluginHelpers.getPluginAvatar(meta) || '🧩'} />,
children: <LocalPluginItem id={identifier} />,
desc: pluginHelpers.getPluginDesc(meta),
label: (
<Flexbox align={'center'} gap={8} horizontal>
{pluginHelpers.getPluginTitle(meta)}
<Tag bordered={false} color={'gold'}>
{t('list.item.local.title', { ns: 'plugin' })}
</Tag>
</Flexbox>
),
minWidth: undefined,
tag: identifier,
}));

return (
<>
<DevModal
onOpenChange={setModal}
onSave={saveToDevList}
onValueChange={updateNewDevPlugin}
open={showModal}
/>
<Form
items={[
{
children: isEmpty ? loadingList : [...devList, ...list],
extra: (
<Tooltip title={t('settingPlugin.addTooltip')}>
<Button
icon={<Icon icon={LucideBlocks} />}
onClick={(e) => {
e.stopPropagation();
setModal(true);
}}
size={'small'}
>
{t('settingPlugin.add')}
</Button>
</Tooltip>
),
icon: LucideStore,
title: t('settingPlugin.title'),
},
]}
{...FORM_STYLE}
/>{' '}
</>
);
});

export default MarketList;
76 changes: 76 additions & 0 deletions src/features/AgentSetting/AgentPlugin/PluginSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { Form, ItemGroup, Markdown } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { ToyBrick } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';

import { FORM_STYLE } from '@/const/layoutTokens';
import { transformPluginSettings } from '@/features/PluginSettings';
import PluginSettingRender from '@/features/PluginSettings/PluginSettingRender';
import { pluginHelpers, pluginSelectors, usePluginStore } from '@/store/plugin';

import { useStore } from '../store';

const useStyles = createStyles(({ css, token }) => ({
md: css`
p {
color: ${token.colorTextDescription};
}
`,
}));

const PluginSettings = memo(() => {
const { t } = useTranslation('setting');
const { styles } = useStyles();

const [plugins] = useStore((s) => [s.config.plugins || [], !!s.config.plugins]);

const [useFetchPluginList, updatePluginSettings] = usePluginStore((s) => [
s.useFetchPluginList,
s.updatePluginSettings,
]);
const pluginList = usePluginStore(pluginSelectors.pluginList);

const pluginManifestMap = usePluginStore((s) => s.pluginManifestMap, isEqual);
const pluginsSettings = usePluginStore((s) => s.pluginsSettings, isEqual);
useFetchPluginList();

const isEmpty = pluginList.length === 0;

const pluginsConfig = isEmpty
? []
: (plugins
.map((identifier) => {
const item = pluginList.find((i) => i.identifier === identifier);

if (!item) return null;
const manifest = pluginManifestMap[identifier];

if (!manifest?.settings) return null;

return {
children: transformPluginSettings(manifest.settings).map((item) => ({
...item,
children: (
<PluginSettingRender
defaultValue={pluginsSettings[identifier]?.[item.name]}
format={item.format}
onChange={(value) => {
updatePluginSettings(identifier, { [item.name]: value });
}}
type={item.type as any}
/>
),
desc: item.desc && <Markdown className={styles.md}>{item.desc}</Markdown>,
})),
icon: ToyBrick,
title: t('settingPlugin.config', { id: pluginHelpers.getPluginTitle(item.meta) }),
};
})
.filter(Boolean) as unknown as ItemGroup[]);

return <Form items={pluginsConfig} {...FORM_STYLE} />;
});

export default PluginSettings;
Loading

0 comments on commit 57b2438

Please sign in to comment.