feat: Add new components and features for AgentMeta

The changes include renaming a file and creating two new files. The code also adds new components and updates existing components in the "AgentMeta" feature. These changes are related to the configuration and metadata of an agent in a chat application. Autocomplete functionality is added to some input fields. The code includes React components for agent settings, such as metadata, configuration, and plugins. The code snippet shows the implementation of a chat editing page with various components and features.
This commit is contained in:
canisminor1990
2023-07-31 15:41:15 +08:00
parent c9ecce1d7a
commit 1232d95a92
14 changed files with 357 additions and 282 deletions

View File

@@ -0,0 +1,48 @@
import data from '@emoji-mart/data';
import i18n from '@emoji-mart/data/i18n/zh.json';
import Picker from '@emoji-mart/react';
import { Avatar } from '@lobehub/ui';
import { Popover } from 'antd';
import { memo } from 'react';
import { DEFAULT_AVATAR, DEFAULT_BACKGROUND_COLOR } from '@/const/meta';
import { useStyles } from './style';
export interface EmojiPickerProps {
avatar?: string;
backgroundColor?: string;
onChange: (emoji: string) => void;
}
const EmojiPicker = memo<EmojiPickerProps>(
({ avatar = DEFAULT_AVATAR, backgroundColor = DEFAULT_BACKGROUND_COLOR, onChange }) => {
const { styles } = useStyles();
return (
<Popover
content={
<div className={styles.picker}>
<Picker
data={data}
i18n={i18n}
locale={'zh'}
onEmojiSelect={(e: any) => onChange(e.native)}
skinTonePosition={'none'}
theme={'auto'}
/>
</div>
}
placement={'left'}
rootClassName={styles.popover}
trigger={'click'}
>
<div className={styles.avatar} style={{ width: 'fit-content' }}>
<Avatar avatar={avatar} background={backgroundColor} size={44} />
</div>
</Popover>
);
},
);
export default EmojiPicker;

View File

@@ -0,0 +1,28 @@
import { createStyles } from 'antd-style';
import chroma from 'chroma-js';
export const useStyles = createStyles(({ css, token, prefixCls }) => ({
avatar: css`
border-radius: 50%;
transition: scale 400ms ${token.motionEaseOut}, box-shadow 100ms ${token.motionEaseOut};
&:hover {
box-shadow: 0 0 0 3px ${token.colorText};
}
&:active {
scale: 0.8;
}
`,
picker: css`
em-emoji-picker {
--rgb-accent: ${chroma(token.colorPrimary) .rgb() .join(',')};
--shadow: none;
}
`,
popover: css`
.${prefixCls}-popover-inner {
padding: 0;
}
`,
}));

View File

@@ -1,16 +1,14 @@
import { Form, ItemGroup } from '@lobehub/ui';
import { ConfigProvider, Input, Segmented, Select, Switch } from 'antd';
import { useTheme } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { debounce } from 'lodash-es';
import { BrainCog, MessagesSquare } from 'lucide-react';
import { useMemo } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import SliderWithInput from 'src/components/SliderWithInput';
import { shallow } from 'zustand/shallow';
import { FORM_STYLE } from '@/const/layoutTokens';
import { agentSelectors, useSessionStore } from '@/store/session';
import { AgentAction } from '@/store/session/slices/agentConfig';
import { LanguageModel } from '@/types/llm';
import type { LobeAgentConfig } from '@/types/session';
@@ -20,14 +18,15 @@ type SettingItemGroup = ItemGroup & {
}[];
};
const AgentConfig = () => {
export interface AgentConfigProps {
config: LobeAgentConfig;
updateConfig: AgentAction['updateAgentConfig'];
}
const AgentConfig = memo<AgentConfigProps>(({ config, updateConfig }) => {
const { t } = useTranslation('setting');
const theme = useTheme();
const config = useSessionStore(agentSelectors.currentAgentConfigSafe, isEqual);
const [updateAgentConfig] = useSessionStore((s) => [s.updateAgentConfig], shallow);
const chat: SettingItemGroup = useMemo(
() => ({
children: [
@@ -165,11 +164,11 @@ const AgentConfig = () => {
<Form
initialValues={config}
items={[chat, model]}
onValuesChange={debounce(updateAgentConfig, 100)}
onValuesChange={debounce(updateConfig, 100)}
{...FORM_STYLE}
/>
</ConfigProvider>
);
};
});
export default AgentConfig;

View File

@@ -6,8 +6,8 @@ import { memo } from 'react';
import { useTranslation } from 'react-i18next';
export interface AutoGenerateInputProps extends InputProps {
loading: boolean;
onGenerate: () => void;
loading?: boolean;
onGenerate?: () => void;
}
const AutoGenerateInput = memo<AutoGenerateInputProps>(({ loading, onGenerate, ...props }) => {
@@ -17,18 +17,20 @@ const AutoGenerateInput = memo<AutoGenerateInputProps>(({ loading, onGenerate, .
return (
<Input
suffix={
<ActionIcon
active
icon={Wand2}
loading={loading}
onClick={onGenerate}
size={'small'}
style={{
color: theme.colorInfo,
marginRight: -4,
}}
title={t('autoGenerate')}
/>
onGenerate && (
<ActionIcon
active
icon={Wand2}
loading={loading}
onClick={onGenerate}
size={'small'}
style={{
color: theme.colorInfo,
marginRight: -4,
}}
title={t('autoGenerate')}
/>
)
}
type={'block'}
{...props}

View File

@@ -0,0 +1,27 @@
import { Swatches, primaryColorsSwatches } from '@lobehub/ui';
import { memo } from 'react';
import { DEFAULT_BACKGROUND_COLOR } from '@/const/meta';
interface BackgroundSwatchesProps {
backgroundColor?: string;
onChange: (color: string) => void;
}
const BackgroundSwatches = memo<BackgroundSwatchesProps>(
({ backgroundColor = DEFAULT_BACKGROUND_COLOR, onChange }) => {
const handleSelect = (v: any) => {
onChange(v || DEFAULT_BACKGROUND_COLOR);
};
return (
<Swatches
activeColor={backgroundColor}
colors={primaryColorsSwatches}
onSelect={handleSelect}
/>
);
},
);
export default BackgroundSwatches;

View File

@@ -0,0 +1,124 @@
import { Form, type FormItemProps, Icon, type ItemGroup, Tooltip } from '@lobehub/ui';
import { Button } from 'antd';
import { UserCircle, Wand2 } from 'lucide-react';
import { ReactNode, memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import EmojiPicker from '@/components/EmojiPicker';
import { FORM_STYLE } from '@/const/layoutTokens';
import { AgentAction, SessionLoadingState } from '@/store/session/slices/agentConfig';
import { MetaData } from '@/types/meta';
import { LobeAgentConfig } from '@/types/session';
import AutoGenerateInput from './AutoGenerateInput';
import BackgroundSwatches from './BackgroundSwatches';
export interface AgentMetaProps {
autocomplete?: {
autocompleteMeta: AgentAction['autocompleteMeta'];
autocompleteSessionAgentMeta: AgentAction['autocompleteSessionAgentMeta'];
id: string | null;
loading: SessionLoadingState;
};
config: LobeAgentConfig;
meta: MetaData;
updateMeta: AgentAction['updateAgentMeta'];
}
const AgentMeta = memo<AgentMetaProps>(({ config, meta, updateMeta, autocomplete }) => {
const { t } = useTranslation('setting');
const hasSystemRole = useMemo(() => !!config.systemRole, [config]);
let extra: ReactNode | undefined;
const basic = [
{
key: 'title',
label: t('settingAgent.name.title'),
placeholder: t('settingAgent.name.placeholder'),
},
{
key: 'description',
label: t('settingAgent.description.title'),
placeholder: t('settingAgent.description.placeholder'),
},
// { key: 'tag', label: t('agentTag'), placeholder: t('agentTagPlaceholder') },
];
const autocompleteItems: FormItemProps[] = basic.map((item) => ({
children: (
<AutoGenerateInput
loading={autocomplete?.loading[item.key as keyof SessionLoadingState]}
onChange={(e) => {
updateMeta({ [item.key]: e.target.value });
}}
onGenerate={() => {
autocomplete?.autocompleteMeta(item.key as keyof typeof meta);
}}
placeholder={item.placeholder}
value={meta[item.key as keyof typeof meta]}
/>
),
label: item.label,
}));
if (autocomplete) {
const { autocompleteSessionAgentMeta, loading, id } = autocomplete;
extra = (
<Tooltip title={t('autoGenerateTooltip', { ns: 'common' })}>
<Button
disabled={!hasSystemRole}
icon={<Icon icon={Wand2} />}
loading={Object.values(loading).some((i) => !!i)}
onClick={(e: any) => {
e.stopPropagation();
if (!id) return;
autocompleteSessionAgentMeta(id, true);
}}
size={'small'}
>
{t('autoGenerate', { ns: 'common' })}
</Button>
</Tooltip>
);
}
const metaData: ItemGroup = useMemo(
() => ({
children: [
{
children: (
<EmojiPicker
avatar={meta.avatar}
backgroundColor={meta.backgroundColor}
onChange={(avatar) => updateMeta({ avatar })}
/>
),
label: t('settingAgent.avatar.title'),
minWidth: undefined,
},
{
children: (
<BackgroundSwatches
backgroundColor={meta.backgroundColor}
onChange={(backgroundColor) => updateMeta({ backgroundColor })}
/>
),
label: t('settingAgent.backgroundColor.title'),
minWidth: undefined,
},
...autocompleteItems,
],
extra: extra,
icon: UserCircle,
title: t('settingAgent.title'),
}),
[autocompleteItems, extra, meta],
);
return <Form items={[metaData]} {...FORM_STYLE} />;
});
export default AgentMeta;

View File

@@ -1,22 +1,22 @@
import { Avatar, Form, ItemGroup } from '@lobehub/ui';
import { Switch } from 'antd';
import isEqual from 'fast-deep-equal';
import { ToyBrick } from 'lucide-react';
import { useMemo } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { shallow } from 'zustand/shallow';
import { FORM_STYLE } from '@/const/layoutTokens';
import { PluginsMap } from '@/plugins';
import { agentSelectors, useSessionStore } from '@/store/session';
import { AgentAction } from '@/store/session/slices/agentConfig';
import { LobeAgentConfig } from '@/types/session';
const PluginList = () => {
export interface AgentPluginProps {
config: LobeAgentConfig;
updateConfig: AgentAction['toggleAgentPlugin'];
}
const AgentPlugin = memo<AgentPluginProps>(({ config, updateConfig }) => {
const { t } = useTranslation('setting');
const config = useSessionStore(agentSelectors.currentAgentConfigSafe, isEqual);
const toggleAgentPlugin = useSessionStore((s) => s.toggleAgentPlugin, shallow);
const plugin: ItemGroup = useMemo(
() => ({
children: Object.values(PluginsMap).map((item) => ({
@@ -24,7 +24,7 @@ const PluginList = () => {
children: (
<Switch
checked={!config.plugins ? false : config.plugins.includes(item.name)}
onChange={() => toggleAgentPlugin(item.name)}
onChange={() => updateConfig(item.name)}
/>
),
desc: item.schema.description,
@@ -39,6 +39,6 @@ const PluginList = () => {
);
return <Form items={[plugin]} {...FORM_STYLE} />;
};
});
export default PluginList;
export default AgentPlugin;

View File

@@ -1,13 +1,14 @@
import { CodeEditor, FormGroup, TokenTag } from '@lobehub/ui';
import { createStyles } from 'antd-style';
import { encode } from 'gpt-tokenizer';
import { Bot } from 'lucide-react';
import { memo } from 'react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { shallow } from 'zustand/shallow';
import { FORM_STYLE } from '@/const/layoutTokens';
import { ModelTokens } from '@/const/modelTokens';
import { agentSelectors, chatSelectors, useSessionStore } from '@/store/session';
import { AgentAction } from '@/store/session/slices/agentConfig';
import { LobeAgentConfig } from '@/types/session';
export const useStyles = createStyles(({ css, token }) => ({
input: css`
@@ -22,18 +23,16 @@ export const useStyles = createStyles(({ css, token }) => ({
`,
}));
const AgentPrompt = memo(() => {
export interface AgentPromptProps {
config: LobeAgentConfig;
updateConfig: AgentAction['updateAgentConfig'];
}
const AgentPrompt = memo<AgentPromptProps>(({ config, updateConfig }) => {
const { t } = useTranslation('setting');
const [systemRole, model, systemTokenCount, updateAgentConfig] = useSessionStore(
(s) => [
agentSelectors.currentAgentSystemRole(s),
agentSelectors.currentAgentModel(s),
chatSelectors.systemRoleTokenCount(s),
s.updateAgentConfig,
],
shallow,
);
const { systemRole, model } = config;
const systemTokenCount = useMemo(() => encode(systemRole || '').length, [systemRole]);
return (
<FormGroup
@@ -46,9 +45,7 @@ const AgentPrompt = memo(() => {
>
<CodeEditor
language={'md'}
onValueChange={(e) => {
updateAgentConfig({ systemRole: e });
}}
onValueChange={(e) => updateConfig({ systemRole: e })}
placeholder={t('settingAgent.name.placeholder')}
resize={false}
style={{ marginTop: 16 }}

View File

@@ -0,0 +1,4 @@
export { default as AgentConfig } from './AgentConfig';
export { default as AgentMeta } from './AgentMeta';
export { default as AgentPlugin } from './AgentPlugin';
export { default as AgentPrompt } from './AgentPrompt';

View File

@@ -0,0 +1,40 @@
{
"exportType": "agents",
"state": {
"sessions": {
"9a2def55-ff50-4f7f-ad6c-b97dac817193": {
"chats": {},
"config": {
"model": "gpt-3.5-turbo",
"params": { "temperature": 0.6 },
"systemRole": "你是一名专业的前端。擅长书写 Typescript JSDoc 代码,代码的示例如下:\n\n```ts\ninterface Props {\n/**\n* 尺寸\n* */\nloading: boolean;\n/**\n* 头像形状\n* @default 'square'\n* @enum [\"square\",\"circle\"]\n* @enumNames [ \"方形\", \"圆形\"]\n*/\nshape?: 'square' | 'circle';\n/**\n* 返回事件\n* @ignore\n*/\nonBack: () => void;\n/**\n* 选择路由的回调函数\n* @param key - 选中的路由\n* @ignore\n*/\nonSelect?: (key: string) => any;\n/**\n* 点击事件回调函数\n* @ignore\n*/\nonClick?: () => void;\n/**\n* 引用\n* @ignore\n*/\nref: any;\n/**\n* Tooltip 提示框位置\n* @enum ['top', 'left', 'right', 'bottom', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'leftTop', 'leftBottom', 'rightTop', 'rightBottom']\n* @enumNames ['上', '左', '右', '下', '左上', '右上', '左下', '右下', '左上', '左下', '右上', '右下']\n* @default 'top'\n*/\nplacement?: TooltipPlacement;\n}\n```\n\n\n接下来用户会输入一串 interface 代码,需要你补全 jsdoc。其中接口的类型不可改变"
},
"createAt": 1690184262713,
"id": "9a2def55-ff50-4f7f-ad6c-b97dac817193",
"meta": { "backgroundColor": "#ec5e41", "avatar": "🥲" },
"type": "agent",
"updateAt": 1690184262713,
"topics": {}
},
"744fe028-c8a2-410c-8b0a-66cfa00d647b": {
"chats": {},
"config": {
"model": "gpt-3.5-turbo",
"params": { "temperature": 0.6 },
"systemRole": "- dafdsafa\n- da- dafdsafa\n- da- dafdsafa\n- da- dafdsafa\n- da- dafdsafa\n- da"
},
"createAt": 1690184730185,
"id": "744fe028-c8a2-410c-8b0a-66cfa00d647b",
"meta": {
"backgroundColor": "#ffef5c",
"title": "dafaa",
"description": "safsafsafsafsafsafsfasfas"
},
"type": "agent",
"updateAt": 1690184730185,
"topics": {}
}
}
},
"version": 1
}

View File

@@ -1,31 +0,0 @@
import { Swatches, primaryColorsSwatches } from '@lobehub/ui';
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
import { DEFAULT_BACKGROUND_COLOR } from '@/const/meta';
import { agentSelectors, useSessionStore } from '@/store/session';
const BackgroundSwatches = memo(() => {
const [backgroundColor, updateAgentMeta] = useSessionStore(
(s) => [agentSelectors.currentAgentBackgroundColor(s), s.updateAgentMeta],
shallow,
);
const handleSelect = (v: any) => {
if (v) {
updateAgentMeta({ backgroundColor: v });
} else {
updateAgentMeta({ backgroundColor: DEFAULT_BACKGROUND_COLOR });
}
};
return (
<Swatches
activeColor={backgroundColor}
colors={primaryColorsSwatches}
onSelect={handleSelect}
/>
);
});
export default BackgroundSwatches;

View File

@@ -1,78 +0,0 @@
import data from '@emoji-mart/data';
import i18n from '@emoji-mart/data/i18n/zh.json';
import Picker from '@emoji-mart/react';
import { Avatar } from '@lobehub/ui';
import { Popover } from 'antd';
import { createStyles } from 'antd-style';
import chroma from 'chroma-js';
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
import { useSessionStore } from '@/store/session';
import { agentSelectors } from '@/store/session/selectors';
const useStyles = createStyles(({ css, token, prefixCls }) => ({
avatar: css`
border-radius: 50%;
transition: scale 400ms ${token.motionEaseOut}, box-shadow 100ms ${token.motionEaseOut};
&:hover {
box-shadow: 0 0 0 3px ${token.colorText};
}
&:active {
scale: 0.8;
}
`,
picker: css`
em-emoji-picker {
--rgb-accent: ${chroma(token.colorPrimary) .rgb() .join(',')};
--shadow: none;
}
`,
popover: css`
.${prefixCls}-popover-inner {
padding: 0;
}
`,
}));
const EmojiPicker = () => {
const { styles } = useStyles();
const [avatar, backgroundColor, updateAgentMeta] = useSessionStore(
(s) => [
agentSelectors.currentAgentAvatar(s),
agentSelectors.currentAgentBackgroundColor(s),
s.updateAgentMeta,
],
shallow,
);
return (
<Popover
content={
<div className={styles.picker}>
<Picker
data={data}
i18n={i18n}
locale={'zh'}
onEmojiSelect={(e: any) => {
updateAgentMeta({ avatar: e.native });
}}
skinTonePosition={'none'}
theme={'auto'}
/>
</div>
}
placement={'left'}
rootClassName={styles.popover}
trigger={'click'}
>
<div className={styles.avatar} style={{ width: 'fit-content' }}>
<Avatar avatar={avatar} background={backgroundColor} size={44} />
</div>
</Popover>
);
};
export default memo(EmojiPicker);

View File

@@ -1,111 +0,0 @@
import { Form, Icon, type ItemGroup, Tooltip } from '@lobehub/ui';
import { Button } from 'antd';
import isEqual from 'fast-deep-equal';
import { UserCircle, Wand2 } from 'lucide-react';
import { memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { shallow } from 'zustand/shallow';
import { FORM_STYLE } from '@/const/layoutTokens';
import { agentSelectors, useSessionStore } from '@/store/session';
import AutoGenerateInput from './AutoGenerateInput';
import BackgroundSwatches from './BackgroundSwatches';
import EmojiPicker from './EmojiPicker';
const AgentMeta = memo(() => {
const { t } = useTranslation('setting');
const metaData = useSessionStore(agentSelectors.currentAgentMeta, isEqual);
const [
autocompleteMeta,
autocompleteSessionAgentMeta,
loading,
updateAgentMeta,
id,
hasSystemRole,
] = useSessionStore(
(s) => [
s.autocompleteMeta,
s.autocompleteSessionAgentMeta,
s.autocompleteLoading,
s.updateAgentMeta,
s.activeId,
agentSelectors.hasSystemRole(s),
],
shallow,
);
const basic = [
{
key: 'title',
label: t('settingAgent.name.title'),
placeholder: t('settingAgent.name.placeholder'),
},
{
key: 'description',
label: t('settingAgent.description.title'),
placeholder: t('settingAgent.description.placeholder'),
},
// { key: 'tag', label: t('agentTag'), placeholder: t('agentTagPlaceholder') },
];
const meta: ItemGroup = useMemo(
() => ({
children: [
{
children: <EmojiPicker />,
label: t('settingAgent.avatar.title'),
minWidth: undefined,
},
{
children: <BackgroundSwatches />,
label: t('settingAgent.backgroundColor.title'),
minWidth: undefined,
},
...basic.map((item) => ({
children: (
<AutoGenerateInput
loading={loading[item.key as keyof typeof loading]}
onChange={(e) => {
updateAgentMeta({ [item.key]: e.target.value });
}}
onGenerate={() => {
autocompleteMeta(item.key as keyof typeof metaData);
}}
placeholder={item.placeholder}
value={metaData[item.key as keyof typeof metaData]}
/>
),
label: item.label,
})),
],
extra: (
<Tooltip title={t('autoGenerateTooltip', { ns: 'common' })}>
<Button
disabled={!hasSystemRole}
icon={<Icon icon={Wand2} />}
loading={Object.values(loading).some((i) => !!i)}
onClick={(e: any) => {
e.stopPropagation();
console.log(id);
if (!id) return;
autocompleteSessionAgentMeta(id, true);
}}
size={'small'}
>
{t('autoGenerate', { ns: 'common' })}
</Button>
</Tooltip>
),
icon: UserCircle,
title: t('settingAgent.title'),
}),
[basic, metaData],
);
return <Form items={[meta]} {...FORM_STYLE} />;
});
export default AgentMeta;

View File

@@ -1,3 +1,4 @@
import isEqual from 'fast-deep-equal';
import Head from 'next/head';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
@@ -6,20 +7,40 @@ import { shallow } from 'zustand/shallow';
import HeaderSpacing from '@/components/HeaderSpacing';
import { HEADER_HEIGHT } from '@/const/layoutTokens';
import { AgentConfig, AgentMeta, AgentPlugin, AgentPrompt } from '@/features/AgentSetting';
import { agentSelectors, useSessionStore } from '@/store/session';
import { genSiteHeadTitle } from '@/utils/genSiteHeadTitle';
import ChatLayout from '../../layout';
import AgentConfig from './AgentConfig';
import AgentMeta from './AgentMeta';
import AgentPlugin from './AgentPlugin';
import AgentPrompt from './AgentPrompt';
import Header from './Header';
const EditPage = memo(() => {
const { t } = useTranslation('setting');
const config = useSessionStore(agentSelectors.currentAgentConfigSafe, isEqual);
const meta = useSessionStore(agentSelectors.currentAgentMeta, isEqual);
const [
updateAgentConfig,
toggleAgentPlugin,
autocompleteMeta,
autocompleteSessionAgentMeta,
loading,
updateAgentMeta,
id,
title,
] = useSessionStore(
(s) => [
s.updateAgentConfig,
s.toggleAgentPlugin,
s.autocompleteMeta,
s.autocompleteSessionAgentMeta,
s.autocompleteLoading,
s.updateAgentMeta,
s.activeId,
agentSelectors.currentAgentTitle(s),
],
shallow,
);
const title = useSessionStore(agentSelectors.currentAgentTitle, shallow);
const pageTitle = genSiteHeadTitle(t('header.sessionWithName', { name: title }));
return (
@@ -31,10 +52,15 @@ const EditPage = memo(() => {
<Header />
<Flexbox align={'center'} flex={1} gap={16} padding={24} style={{ overflow: 'auto' }}>
<HeaderSpacing height={HEADER_HEIGHT - 16} />
<AgentPrompt />
<AgentMeta />
<AgentConfig />
<AgentPlugin />
<AgentPrompt config={config} updateConfig={updateAgentConfig} />
<AgentMeta
autocomplete={{ autocompleteMeta, autocompleteSessionAgentMeta, id, loading }}
config={config}
meta={meta}
updateMeta={updateAgentMeta}
/>
<AgentConfig config={config} updateConfig={updateAgentConfig} />
<AgentPlugin config={config} updateConfig={toggleAgentPlugin} />
</Flexbox>
</ChatLayout>
</>