Skip to content

Commit

Permalink
Merge pull request lobehub#138 from lobehub/plugin-dx
Browse files Browse the repository at this point in the history
插件开发体验优化
  • Loading branch information
arvinxx authored Sep 2, 2023
2 parents d75ab9e + 82e3beb commit b5482a1
Show file tree
Hide file tree
Showing 19 changed files with 397 additions and 232 deletions.
18 changes: 10 additions & 8 deletions src/features/AgentSetting/AgentPlugin/LocalPluginItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ const MarketList = memo<{ id: string }>(({ id }) => {
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 [useFetchPluginList, fetchPluginManifest, deleteCustomPlugin, updateCustomPlugin] =
usePluginStore((s) => [
s.useFetchPluginList,
s.fetchPluginManifest,
s.deleteCustomPlugin,
s.updateCustomPlugin,
]);

const pluginManifestLoading = usePluginStore((s) => s.pluginManifestLoading, isEqual);
const devPlugin = usePluginStore(pluginSelectors.getDevPluginById(id), isEqual);
Expand All @@ -31,15 +33,15 @@ const MarketList = memo<{ id: string }>(({ id }) => {
return (
<>
<DevModal
mode={'edit'}
onDelete={() => {
dispatchDevPluginList({ id, type: 'deleteItem' });
deleteCustomPlugin(id);
}}
onOpenChange={setModal}
onSave={(value) => {
dispatchDevPluginList({ id, plugin: value, type: 'updateItem' });
updateCustomPlugin(id, value);
}}
open={showModal}
showDelete
value={devPlugin}
/>

Expand Down
42 changes: 29 additions & 13 deletions src/features/AgentSetting/AgentPlugin/MarketList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,33 @@ const MarketList = memo(() => {
// setModal(true);
// }, []);

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

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

useFetchPluginList();

const togglePlugin = async (pluginId: string, fetchManifest?: boolean) => {
toggleAgentPlugin(pluginId);
if (fetchManifest) {
await fetchPluginManifest(pluginId);
}
};

// =========== Skeleton Loading =========== //
const loadingItem = {
avatar: (
<Skeleton
Expand All @@ -77,11 +88,12 @@ const MarketList = memo(() => {
/>
),
};

const loadingList = [loadingItem, loadingItem, loadingItem];

const isEmpty = pluginList.length === 0;

// =========== Plugin List =========== //

const list = pluginList.map(({ identifier, meta }) => ({
avatar: <Avatar avatar={meta.avatar} />,
children: (
Expand All @@ -92,10 +104,7 @@ const MarketList = memo(() => {
}
loading={pluginManifestLoading[identifier]}
onChange={(checked) => {
updateConfig(identifier);
if (checked) {
fetchPluginManifest(identifier);
}
togglePlugin(identifier, checked);
}}
/>
),
Expand All @@ -105,7 +114,9 @@ const MarketList = memo(() => {
tag: identifier,
}));

const devList = devPluginList.map(({ identifier, meta }) => ({
// =========== Custom Plugin List =========== //

const customList = customPluginList.map(({ identifier, meta }) => ({
avatar: <Avatar avatar={pluginHelpers.getPluginAvatar(meta) || '🧩'} />,
children: <LocalPluginItem id={identifier} />,
desc: pluginHelpers.getPluginDesc(meta),
Expand All @@ -125,14 +136,19 @@ const MarketList = memo(() => {
<>
<DevModal
onOpenChange={setModal}
onSave={saveToDevList}
onSave={async (devPlugin) => {
// 先保存
saveToDevList(devPlugin);
// 再开启插件
await togglePlugin(devPlugin.identifier, true);
}}
onValueChange={updateNewDevPlugin}
open={showModal}
/>
<Form
items={[
{
children: isEmpty ? loadingList : [...devList, ...list],
children: isEmpty ? loadingList : [...customList, ...list],
extra: (
<Tooltip title={t('settingPlugin.addTooltip')}>
<Button
Expand Down
236 changes: 150 additions & 86 deletions src/features/PluginDevModal/ManifestForm.tsx
Original file line number Diff line number Diff line change
@@ -1,96 +1,160 @@
import { pluginManifestSchema } from '@lobehub/chat-plugin-sdk';
import { Form, FormItemProps, Input, Tooltip } from '@lobehub/ui';
import { FormInstance, Radio } from 'antd';
import { memo } from 'react';
import { LobeChatPluginManifest, pluginManifestSchema } from '@lobehub/chat-plugin-sdk';
import { ActionIcon, Form, FormItemProps, Highlighter, Input, Tooltip } from '@lobehub/ui';
import { FormInstance, Popover, Radio } from 'antd';
import { FileCode, RotateCwIcon } from 'lucide-react';
import { memo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit';

const ManifestForm = memo<{ form: FormInstance; mode: 'url' | 'local' }>(({ form, mode }) => {
const { t } = useTranslation('plugin');
const ManifestForm = memo<{ form: FormInstance; mode?: 'url' | 'local' }>(
({ form, mode = 'url' }) => {
const { t } = useTranslation('plugin');

const isUrl = mode === 'url';
const [manifest, setManifest] = useState<LobeChatPluginManifest>();

const configItem: FormItemProps[] = isUrl
? [
{
children: <Input placeholder={'http://localhost/manifest.json'} />,
desc: t('dev.meta.manifest.desc'),
hasFeedback: true,
label: t('dev.meta.manifest.label'),
name: 'manifest',
required: true,
rules: [
{ required: true },
{
message: t('dev.meta.manifest.urlError'),
pattern: /^https?:\/\/.*/,
},
{
message: t('dev.meta.manifest.invalid'),
validator: async (_, value) => {
const res = await fetch(value);
if (!res.ok) return true;
const isUrl = mode === 'url';

const json = await res.json();
pluginManifestSchema.parse(json);
const configItem: FormItemProps[] = isUrl
? [
{
children: (
<Input
placeholder={'http://localhost:3400/manifest-dev.json'}
suffix={
manifest && (
<ActionIcon
icon={RotateCwIcon}
onClick={(e) => {
e.stopPropagation();
form.validateFields(['manifest']);
}}
size={'small'}
title={t('dev.meta.manifest.refresh')}
/>
)
}
/>
),
extra: (
<Flexbox horizontal justify={'space-between'} style={{ marginTop: 8 }}>
{t('dev.meta.manifest.desc')}
{manifest && (
<Popover
arrow={false}
content={
<Highlighter language={'json'}>
{JSON.stringify(manifest, null, 2)}
</Highlighter>
}
placement={'right'}
style={{ width: 400 }}
title={'Manifest JSON'}
trigger={'click'}
>
<ActionIcon
icon={FileCode}
size={'small'}
title={t('dev.meta.manifest.preview')}
/>
</Popover>
)}
</Flexbox>
),
// extra: <div>123</div>,
hasFeedback: true,
label: t('dev.meta.manifest.label'),
name: 'manifest',
required: true,
rules: [
{ required: true },
{
message: t('dev.meta.manifest.urlError'),
pattern: /^https?:\/\/.*/,
},
},
],
},
]
: // TODO: 后续做成本地配置模式
[
{
children: <Input placeholder={'searchEngine'} />,
desc: t('dev.meta.identifier.desc'),
label: t('dev.meta.identifier.label'),
name: 'name',
required: true,
},
{
// message: t('dev.meta.manifest.invalid'),
validator: async (_, value) => {
if (!value) return true;

{
children: <Input placeholder={t('dev.meta.description.placeholder')} />,
desc: t('dev.meta.description.desc'),
label: t('dev.meta.description.label'),
name: 'description',
required: true,
},
{
children: <Input placeholder={'searchEngine'} />,
desc: t('dev.meta.identifier.desc'),
label: t('dev.meta.identifier.label'),
name: 'identifier',
required: true,
},
];
let res: Response;

return (
<Form
form={form}
items={[
{
children: configItem,
extra: (
<Radio.Group
onChange={(v) => {
form.setFieldValue('manifestMode', v.target.value);
}}
size={'small'}
value={mode}
>
<Radio.Button value={'url'}>{t('dev.manifest.mode.url')}</Radio.Button>
<Tooltip title={t('dev.manifest.mode.local-tooltip')}>
<Radio.Button disabled value={'local'}>
{t('dev.manifest.mode.local')}
</Radio.Button>
</Tooltip>
</Radio.Group>
),
title: t('dev.tabs.manifest'),
},
]}
layout={isUrl ? 'vertical' : undefined}
/>
);
});
try {
res = await fetch(value);
} catch {
throw t('dev.meta.manifest.requestError');
}

const json = await res.json().catch(() => {
throw t('dev.meta.manifest.urlError');
});

const valid = pluginManifestSchema.safeParse(json);
if (!valid.success) {
throw t('dev.meta.manifest.jsonInvalid', { error: valid.error });
}

setManifest(json);
form.setFieldValue('identifier', valid.data.identifier);
},
},
],
},
]
: // TODO: 后续做成本地配置模式
[
{
children: <Input placeholder={'searchEngine'} />,
desc: t('dev.meta.identifier.desc'),
label: t('dev.meta.identifier.label'),
name: 'name',
required: true,
},

{
children: <Input placeholder={t('dev.meta.description.placeholder')} />,
desc: t('dev.meta.description.desc'),
label: t('dev.meta.description.label'),
name: 'description',
required: true,
},
{
children: <Input placeholder={'searchEngine'} />,
desc: t('dev.meta.identifier.desc'),
label: t('dev.meta.identifier.label'),
name: 'identifier',
required: true,
},
];

return (
<Form
form={form}
items={[
{
children: configItem,
extra: (
<Radio.Group
onChange={(v) => {
form.setFieldValue('manifestMode', v.target.value);
}}
size={'small'}
value={mode}
>
<Radio.Button value={'url'}>{t('dev.manifest.mode.url')}</Radio.Button>
<Tooltip title={t('dev.manifest.mode.local-tooltip')}>
<Radio.Button disabled value={'local'}>
{t('dev.manifest.mode.local')}
</Radio.Button>
</Tooltip>
</Radio.Group>
),
title: t('dev.tabs.manifest'),
},
]}
layout={isUrl ? 'vertical' : undefined}
/>
);
},
);

export default ManifestForm;
Loading

0 comments on commit b5482a1

Please sign in to comment.