知识库功能开发
parent
327749afbd
commit
8004ee2e79
@ -0,0 +1,16 @@
|
|||||||
|
import { Skeleton } from 'antd';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
const SkeletonLoading = memo(() => (
|
||||||
|
<Flexbox padding={12}>
|
||||||
|
<Skeleton active paragraph={{ width: '70%' }} title={false} />
|
||||||
|
<Skeleton active paragraph={{ width: '40%' }} title={false} />
|
||||||
|
<Skeleton active paragraph={{ width: '80%' }} title={false} />
|
||||||
|
<Skeleton active paragraph={{ width: '30%' }} title={false} />
|
||||||
|
<Skeleton active paragraph={{ width: '50%' }} title={false} />
|
||||||
|
<Skeleton active paragraph={{ width: '70%' }} title={false} />
|
||||||
|
</Flexbox>
|
||||||
|
));
|
||||||
|
|
||||||
|
export default SkeletonLoading;
|
@ -0,0 +1,35 @@
|
|||||||
|
import { Input } from 'antd';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { MAX_GREETING_LENGTH } from '@/constants/common';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
const [greeting, updateAgentConfig] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentGreeting(s),
|
||||||
|
s.updateAgentConfig,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input.TextArea
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
value={greeting}
|
||||||
|
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||||
|
placeholder={t('role.greetTip')}
|
||||||
|
showCount
|
||||||
|
maxLength={MAX_GREETING_LENGTH}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateAgentConfig({ greeting: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,78 @@
|
|||||||
|
import { Upload } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import NextImage from 'next/image';
|
||||||
|
import React, { CSSProperties, memo, useCallback } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AVATAR_COMPRESS_SIZE,
|
||||||
|
AVATAR_IMAGE_SIZE,
|
||||||
|
COVER_COMPRESS_SIZE,
|
||||||
|
DEFAULT_AGENT_AVATAR_URL,
|
||||||
|
} from '@/constants/common';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
import { createUploadImageHandler } from '@/utils/common';
|
||||||
|
import { imageToBase64 } from '@/utils/imageToBase64';
|
||||||
|
|
||||||
|
const useStyle = createStyles(
|
||||||
|
({ css, token }) => css`
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
interface AvatarWithUploadProps {
|
||||||
|
id?: string;
|
||||||
|
size?: number;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<AvatarWithUploadProps>(({ size = AVATAR_IMAGE_SIZE, style, id }) => {
|
||||||
|
const { styles } = useStyle();
|
||||||
|
const [avatar, updateAgentMeta] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentMeta(s)?.avatar,
|
||||||
|
s.updateAgentMeta,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleUploadAvatar = useCallback(
|
||||||
|
createUploadImageHandler((avatar) => {
|
||||||
|
const img = new Image();
|
||||||
|
img.src = avatar;
|
||||||
|
img.addEventListener('load', () => {
|
||||||
|
const avatar = imageToBase64({ img, size: AVATAR_COMPRESS_SIZE });
|
||||||
|
const cover = imageToBase64({ img, size: COVER_COMPRESS_SIZE });
|
||||||
|
updateAgentMeta({ avatar, cover });
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles}
|
||||||
|
id={id}
|
||||||
|
style={{ maxHeight: AVATAR_IMAGE_SIZE, maxWidth: AVATAR_IMAGE_SIZE, ...style }}
|
||||||
|
>
|
||||||
|
<Upload beforeUpload={handleUploadAvatar} itemRender={() => void 0} maxCount={1}>
|
||||||
|
<NextImage
|
||||||
|
alt={avatar ? 'userAvatar' : 'LobeVidol'}
|
||||||
|
height={size}
|
||||||
|
src={!!avatar ? avatar : DEFAULT_AGENT_AVATAR_URL}
|
||||||
|
unoptimized
|
||||||
|
width={size}
|
||||||
|
/>
|
||||||
|
</Upload>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,35 @@
|
|||||||
|
import { Input } from 'antd';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { MAX_README_LENGTH } from '@/constants/common';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
const [readme, updateAgentMeta] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentMeta(s)?.readme,
|
||||||
|
s.updateAgentMeta,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input.TextArea
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
value={readme}
|
||||||
|
autoSize={{ minRows: 10, maxRows: 10 }}
|
||||||
|
placeholder={t('role.roleReadmeTip')}
|
||||||
|
showCount
|
||||||
|
maxLength={MAX_README_LENGTH}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateAgentMeta({ readme: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,44 @@
|
|||||||
|
import { Select } from 'antd';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
import { RoleCategoryEnum } from '@/types/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const [category, updateAgentMeta] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentMeta(s)?.category,
|
||||||
|
s.updateAgentMeta,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
options={[
|
||||||
|
{ label: t('category.animal'), value: RoleCategoryEnum.ANIMAL },
|
||||||
|
{ label: t('category.anime'), value: RoleCategoryEnum.ANIME },
|
||||||
|
{ label: t('category.book'), value: RoleCategoryEnum.BOOK },
|
||||||
|
{ label: t('category.game'), value: RoleCategoryEnum.GAME },
|
||||||
|
{ label: t('category.history'), value: RoleCategoryEnum.HISTORY },
|
||||||
|
{ label: t('category.movie'), value: RoleCategoryEnum.MOVIE },
|
||||||
|
{ label: t('category.realistic'), value: RoleCategoryEnum.REALISTIC },
|
||||||
|
{ label: t('category.vroid'), value: RoleCategoryEnum.VROID },
|
||||||
|
{ label: t('category.vtuber'), value: RoleCategoryEnum.VTUBER },
|
||||||
|
]}
|
||||||
|
value={category}
|
||||||
|
defaultActiveFirstOption={true}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateAgentMeta({ category: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,34 @@
|
|||||||
|
import { Input } from 'antd';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { MAX_DESCRIPTION_LENGTH } from '@/constants/common';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
const [description, updateAgentMeta] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentMeta(s)?.description,
|
||||||
|
s.updateAgentMeta,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
value={description}
|
||||||
|
placeholder={t('role.roleDescriptionTip')}
|
||||||
|
maxLength={MAX_DESCRIPTION_LENGTH}
|
||||||
|
showCount
|
||||||
|
onChange={(e) => {
|
||||||
|
updateAgentMeta({ description: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,37 @@
|
|||||||
|
import { Select } from 'antd';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
import { GenderEnum } from '@/types/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const [gender, updateAgentMeta] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentMeta(s)?.gender,
|
||||||
|
s.updateAgentMeta,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
options={[
|
||||||
|
{ label: t('gender.male'), value: GenderEnum.MALE },
|
||||||
|
{ label: t('gender.female'), value: GenderEnum.FEMALE },
|
||||||
|
]}
|
||||||
|
value={gender}
|
||||||
|
defaultActiveFirstOption={true}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateAgentMeta({ gender: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,34 @@
|
|||||||
|
import { Input } from 'antd';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { MAX_NAME_LENGTH } from '@/constants/common';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
const [name, updateAgentMeta] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentMeta(s)?.name,
|
||||||
|
s.updateAgentMeta,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
value={name}
|
||||||
|
placeholder={t('role.roleNameTip')}
|
||||||
|
maxLength={MAX_NAME_LENGTH}
|
||||||
|
showCount
|
||||||
|
onChange={(e) => {
|
||||||
|
updateAgentMeta({ name: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,67 @@
|
|||||||
|
import { Form, FormProps } from '@lobehub/ui';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { FORM_STYLE } from '@/constants/token';
|
||||||
|
|
||||||
|
import Greeting from './Greeting';
|
||||||
|
import PreviewWithUpload from './PreviewWithUpload';
|
||||||
|
import ReadMe from './ReadMe';
|
||||||
|
import RoleCategory from './RoleCategory';
|
||||||
|
import RoleDescription from './RoleDescription';
|
||||||
|
import RoleGender from './RoleGender';
|
||||||
|
import RoleName from './RoleName';
|
||||||
|
|
||||||
|
const Info = () => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
const basic: FormProps['items'] = [
|
||||||
|
{
|
||||||
|
label: t('info.avatarLabel'),
|
||||||
|
desc: t('info.avatarDescription'),
|
||||||
|
name: 'avatar',
|
||||||
|
children: <PreviewWithUpload />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('info.nameLabel'),
|
||||||
|
desc: t('info.nameDescription'),
|
||||||
|
name: 'name',
|
||||||
|
children: <RoleName />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('info.descLabel'),
|
||||||
|
desc: t('info.descDescription'),
|
||||||
|
name: 'description',
|
||||||
|
children: <RoleDescription />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('info.greetLabel'),
|
||||||
|
desc: t('info.greetDescription'),
|
||||||
|
name: 'greeting',
|
||||||
|
children: <Greeting />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('info.genderLabel'),
|
||||||
|
desc: t('info.genderDescription'),
|
||||||
|
name: 'gender',
|
||||||
|
children: <RoleGender />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('info.categoryLabel'),
|
||||||
|
desc: t('info.categoryDescription'),
|
||||||
|
name: 'category',
|
||||||
|
children: <RoleCategory />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('info.readmeLabel'),
|
||||||
|
desc: t('info.readmeDescription'),
|
||||||
|
name: 'readme',
|
||||||
|
children: <ReadMe />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <Form form={form} items={basic} itemsType={'flat'} variant={'block'} {...FORM_STYLE} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Info;
|
@ -0,0 +1,23 @@
|
|||||||
|
import { SliderWithInput } from '@lobehub/ui';
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
const FrequencyPenalty = memo(() => {
|
||||||
|
const [frequency_penalty, updateAgentConfig] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentParams(s)?.frequency_penalty,
|
||||||
|
s.updateAgentConfig,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SliderWithInput
|
||||||
|
max={2}
|
||||||
|
min={-2}
|
||||||
|
step={0.1}
|
||||||
|
value={frequency_penalty}
|
||||||
|
onChange={(value) => updateAgentConfig({ params: { frequency_penalty: value } })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default FrequencyPenalty;
|
@ -0,0 +1,79 @@
|
|||||||
|
import { Select, SelectProps } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import isEqual from 'fast-deep-equal';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { ModelItemRender, ProviderItemRender } from '@/components/ModelSelect';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
import { useSettingStore } from '@/store/setting';
|
||||||
|
import { modelProviderSelectors } from '@/store/setting/selectors';
|
||||||
|
import { ModelProviderCard } from '@/types/llm';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, prefixCls }) => ({
|
||||||
|
select: css`
|
||||||
|
&.${prefixCls}-select-dropdown .${prefixCls}-select-item-option-grouped {
|
||||||
|
padding-inline-start: 12px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface ModelOption {
|
||||||
|
label: any;
|
||||||
|
provider: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelSelectProps {
|
||||||
|
onChange?: (props: { model: string; provider: string }) => void;
|
||||||
|
showAbility?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModelSelect = memo<ModelSelectProps>(({ showAbility = true }) => {
|
||||||
|
const enabledList = useSettingStore(
|
||||||
|
modelProviderSelectors.modelProviderListForModelSelect,
|
||||||
|
isEqual,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [model, provider, updateAgentConfig] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentModel(s),
|
||||||
|
agentSelectors.currentAgentProvider(s),
|
||||||
|
s.updateAgentConfig,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { styles } = useStyles();
|
||||||
|
|
||||||
|
const options = useMemo<SelectProps['options']>(() => {
|
||||||
|
const getChatModels = (provider: ModelProviderCard) =>
|
||||||
|
provider.chatModels.map((model) => ({
|
||||||
|
label: <ModelItemRender {...model} showInfoTag={showAbility} />,
|
||||||
|
provider: provider.id,
|
||||||
|
value: `${provider.id}/${model.id}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (enabledList.length === 1) {
|
||||||
|
const provider = enabledList[0];
|
||||||
|
|
||||||
|
return getChatModels(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
return enabledList.map((provider) => ({
|
||||||
|
label: <ProviderItemRender name={provider.name} provider={provider.id} />,
|
||||||
|
options: getChatModels(provider),
|
||||||
|
}));
|
||||||
|
}, [enabledList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
onChange={(value, option) => {
|
||||||
|
const model = value.split('/').slice(1).join('/');
|
||||||
|
updateAgentConfig({ model, provider: (option as unknown as ModelOption).provider });
|
||||||
|
}}
|
||||||
|
options={options}
|
||||||
|
popupClassName={styles.select}
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
|
value={`${provider}/${model}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ModelSelect;
|
@ -0,0 +1,23 @@
|
|||||||
|
import { SliderWithInput } from '@lobehub/ui';
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
const PresencePenalty = memo(() => {
|
||||||
|
const [presence_penalty, updateAgentConfig] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentParams(s)?.presence_penalty,
|
||||||
|
s.updateAgentConfig,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SliderWithInput
|
||||||
|
max={2}
|
||||||
|
min={-2}
|
||||||
|
step={0.1}
|
||||||
|
value={presence_penalty}
|
||||||
|
onChange={(value) => updateAgentConfig({ params: { presence_penalty: value } })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default PresencePenalty;
|
@ -0,0 +1,23 @@
|
|||||||
|
import { SliderWithInput } from '@lobehub/ui';
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
const Temperature = memo(() => {
|
||||||
|
const [temperature, updateAgentConfig] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentParams(s)?.temperature,
|
||||||
|
s.updateAgentConfig,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SliderWithInput
|
||||||
|
max={1}
|
||||||
|
min={0}
|
||||||
|
step={0.1}
|
||||||
|
value={temperature}
|
||||||
|
onChange={(value) => updateAgentConfig({ params: { temperature: value } })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Temperature;
|
@ -0,0 +1,23 @@
|
|||||||
|
import { SliderWithInput } from '@lobehub/ui';
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
const TopP = memo(() => {
|
||||||
|
const [top_p, updateAgentConfig] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentParams(s)?.top_p,
|
||||||
|
s.updateAgentConfig,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SliderWithInput
|
||||||
|
max={1}
|
||||||
|
min={0}
|
||||||
|
step={0.1}
|
||||||
|
value={top_p}
|
||||||
|
onChange={(value) => updateAgentConfig({ params: { top_p: value } })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TopP;
|
@ -0,0 +1,58 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Form, FormProps } from '@lobehub/ui';
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import FrequencyPenalty from '@/app/role/RoleEdit/LangModel/FrequencyPenalty';
|
||||||
|
import ModelSelect from '@/app/role/RoleEdit/LangModel/ModelSelect';
|
||||||
|
import PresencePenalty from '@/app/role/RoleEdit/LangModel/PresencePenalty';
|
||||||
|
import Temperature from '@/app/role/RoleEdit/LangModel/Temperature';
|
||||||
|
import TopP from '@/app/role/RoleEdit/LangModel/TopP';
|
||||||
|
import { FORM_STYLE } from '@/constants/token';
|
||||||
|
|
||||||
|
const LangModel = memo(() => {
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
const model: FormProps['items'] = [
|
||||||
|
{
|
||||||
|
children: <ModelSelect />,
|
||||||
|
desc: t('llm.modelDescription'),
|
||||||
|
label: t('llm.modelLabel'),
|
||||||
|
name: 'model',
|
||||||
|
tag: 'model',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <Temperature />,
|
||||||
|
desc: t('llm.temperatureDescription'),
|
||||||
|
label: t('llm.temperatureLabel'),
|
||||||
|
name: ['params', 'temperature'],
|
||||||
|
tag: 'temperature',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <TopP />,
|
||||||
|
desc: t('llm.topPDescription'),
|
||||||
|
label: t('llm.topPLabel'),
|
||||||
|
name: ['params', 'top_p'],
|
||||||
|
tag: 'top_p',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <PresencePenalty />,
|
||||||
|
desc: t('llm.presencePenaltyDescription'),
|
||||||
|
label: t('llm.presencePenaltyLabel'),
|
||||||
|
name: ['params', 'presence_penalty'],
|
||||||
|
tag: 'presence_penalty',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: <FrequencyPenalty />,
|
||||||
|
desc: t('llm.frequencyPenaltyDescription'),
|
||||||
|
label: t('llm.frequencyPenaltyLabel'),
|
||||||
|
name: ['params', 'frequency_penalty'],
|
||||||
|
tag: 'frequency_penalty',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return <Form items={model} itemsType={'flat'} variant={'block'} {...FORM_STYLE} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default LangModel;
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Input } from 'antd';
|
||||||
|
import React, { CSSProperties, memo, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { MAX_SYSTEM_ROLE_LENGTH } from '@/constants/common';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
const [systemRole, updateAgentConfig] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentItem(s)?.systemRole,
|
||||||
|
s.updateAgentConfig,
|
||||||
|
]);
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
return (
|
||||||
|
<Input.TextArea
|
||||||
|
ref={inputRef}
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
value={systemRole}
|
||||||
|
autoSize={{ minRows: 16 }}
|
||||||
|
placeholder={t('role.inputRoleSetting')}
|
||||||
|
showCount
|
||||||
|
maxLength={MAX_SYSTEM_ROLE_LENGTH}
|
||||||
|
onChange={(e) => {
|
||||||
|
updateAgentConfig({ systemRole: e.target.value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,50 @@
|
|||||||
|
import { Card, Cards } from '@lobehub/ui/mdx';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const [name, updateAgentConfig] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentItem(s)?.meta.name,
|
||||||
|
s.updateAgentConfig,
|
||||||
|
]);
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
return (
|
||||||
|
<Cards style={{ marginTop: 24, ...style }} className={className}>
|
||||||
|
<Card
|
||||||
|
image="https://r2.vidol.chat/common/default.png"
|
||||||
|
title={t('systemRole.defaultLabel', { ns: 'role' })}
|
||||||
|
onClick={() => {
|
||||||
|
updateAgentConfig({
|
||||||
|
systemRole: t('systemRole.default', { ns: 'role', char: name }),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
image="https://r2.vidol.chat/common/genshin.png"
|
||||||
|
title={t('systemRole.geniusLabel', { ns: 'role' })}
|
||||||
|
onClick={() => {
|
||||||
|
updateAgentConfig({
|
||||||
|
systemRole: t('systemRole.genius', { ns: 'role', char: name }),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Card
|
||||||
|
image="https://r2.vidol.chat/common/zzz.png"
|
||||||
|
title={t('systemRole.zzzLabel', { ns: 'role' })}
|
||||||
|
onClick={() => {
|
||||||
|
updateAgentConfig({
|
||||||
|
systemRole: t('systemRole.zzz', { ns: 'role', char: name }),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Cards>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,27 @@
|
|||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import SystemRole from './SystemRole';
|
||||||
|
import Templates from './Templates';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css }) => ({
|
||||||
|
container: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 16px;
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const Info = () => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classNames(styles.container)}>
|
||||||
|
<SystemRole />
|
||||||
|
<Templates />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Info;
|
@ -0,0 +1,167 @@
|
|||||||
|
import { ActionIcon, Form, FormItem, Modal } from '@lobehub/ui';
|
||||||
|
import { VRMExpressionPresetName } from '@pixiv/three-vrm';
|
||||||
|
import { Input, Select } from 'antd';
|
||||||
|
import { Edit2Icon, Plus } from 'lucide-react';
|
||||||
|
import React, { memo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { INPUT_WIDTH_MD, INPUT_WIDTH_SM } from '@/constants/token';
|
||||||
|
import { MAX_TOUCH_ACTION_TEXT_LENGTH } from '@/constants/touch';
|
||||||
|
import { MotionPresetName, motionPresetMap } from '@/libs/emoteController/motionPresetMap';
|
||||||
|
import { useAgentStore } from '@/store/agent';
|
||||||
|
import { TouchAction, TouchAreaEnum } from '@/types/touch';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
index?: number;
|
||||||
|
isEdit?: boolean;
|
||||||
|
touchAction?: TouchAction;
|
||||||
|
touchArea: TouchAreaEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo((props: Props) => {
|
||||||
|
const { touchArea, index, touchAction, isEdit = true } = props;
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
const [updateTouchAction, createTouchAction] = useAgentStore((s) => [
|
||||||
|
s.updateTouchAction,
|
||||||
|
s.createTouchAction,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const showModal = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
form.validateFields().then((values) => {
|
||||||
|
setOpen(false);
|
||||||
|
if (isEdit) {
|
||||||
|
updateTouchAction(touchArea, index!, values);
|
||||||
|
} else {
|
||||||
|
createTouchAction(touchArea, values);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionIcon
|
||||||
|
icon={isEdit ? Edit2Icon : Plus}
|
||||||
|
title={isEdit ? t('actions.edit', { ns: 'chat' }) : t('actions.add', { ns: 'chat' })}
|
||||||
|
onClick={showModal}
|
||||||
|
/>
|
||||||
|
<Modal
|
||||||
|
allowFullscreen
|
||||||
|
onCancel={handleCancel}
|
||||||
|
onOk={handleOk}
|
||||||
|
open={open}
|
||||||
|
width={800}
|
||||||
|
destroyOnClose
|
||||||
|
title={isEdit ? t('touch.editAction') : t('touch.addAction')}
|
||||||
|
okText={t('confirm', { ns: 'common' })}
|
||||||
|
cancelText={t('cancel', { ns: 'common' })}
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
layout="horizontal"
|
||||||
|
requiredMark
|
||||||
|
initialValues={
|
||||||
|
isEdit
|
||||||
|
? touchAction
|
||||||
|
: {
|
||||||
|
expression: VRMExpressionPresetName.Neutral,
|
||||||
|
motion: MotionPresetName.FemaleHappy,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form={form}
|
||||||
|
preserve={false}
|
||||||
|
>
|
||||||
|
<FormItem
|
||||||
|
label={t('info.textLabel')}
|
||||||
|
desc={t('info.textDescription')}
|
||||||
|
name={'text'}
|
||||||
|
rules={[{ required: true, message: t('touch.inputDIYText') }]}
|
||||||
|
>
|
||||||
|
<Input.TextArea
|
||||||
|
placeholder={t('touch.inputActionText')}
|
||||||
|
maxLength={MAX_TOUCH_ACTION_TEXT_LENGTH}
|
||||||
|
showCount
|
||||||
|
autoSize
|
||||||
|
style={{ width: INPUT_WIDTH_MD }}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
label={t('info.emotionLabel')}
|
||||||
|
desc={t('info.emotionDescription')}
|
||||||
|
divider
|
||||||
|
rules={[{ required: true, message: t('touch.inputActionEmotion') }]}
|
||||||
|
name="expression"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: t('touch.expression.natural'),
|
||||||
|
value: VRMExpressionPresetName.Neutral,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.expression.happy'),
|
||||||
|
value: VRMExpressionPresetName.Happy,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.expression.angry'),
|
||||||
|
value: VRMExpressionPresetName.Angry,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.expression.sad'),
|
||||||
|
value: VRMExpressionPresetName.Sad,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.expression.relaxed'),
|
||||||
|
value: VRMExpressionPresetName.Relaxed,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.expression.surprised'),
|
||||||
|
value: VRMExpressionPresetName.Surprised,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.expression.blink'),
|
||||||
|
value: VRMExpressionPresetName.Blink,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.expression.blinkLeft'),
|
||||||
|
value: VRMExpressionPresetName.BlinkLeft,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.expression.blinkRight'),
|
||||||
|
value: VRMExpressionPresetName.BlinkRight,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
style={{ width: INPUT_WIDTH_SM }}
|
||||||
|
defaultActiveFirstOption={true}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem
|
||||||
|
label={t('info.motionLabel')}
|
||||||
|
desc={t('info.motionDescription')}
|
||||||
|
divider
|
||||||
|
rules={[{ required: true, message: t('touch.inputActionEmotion') }]}
|
||||||
|
name="motion"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
options={Object.entries(motionPresetMap).map(([key, value]) => ({
|
||||||
|
label: t(`${value.name}`),
|
||||||
|
value: key,
|
||||||
|
}))}
|
||||||
|
style={{ width: INPUT_WIDTH_SM }}
|
||||||
|
defaultActiveFirstOption={true}
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,32 @@
|
|||||||
|
import { ActionIcon } from '@lobehub/ui';
|
||||||
|
import { Popconfirm } from 'antd';
|
||||||
|
import { XIcon } from 'lucide-react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useAgentStore } from '@/store/agent';
|
||||||
|
import { TouchAreaEnum } from '@/types/touch';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
index: number;
|
||||||
|
touchArea: TouchAreaEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo((props: Props) => {
|
||||||
|
const { touchArea, index } = props;
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
const [removeTouchAction] = useAgentStore((s) => [s.removeTouchAction]);
|
||||||
|
return (
|
||||||
|
<Popconfirm
|
||||||
|
title={t('confirmDel')}
|
||||||
|
key="delete"
|
||||||
|
okText={t('confirm')}
|
||||||
|
cancelText={t('cancel')}
|
||||||
|
onConfirm={() => {
|
||||||
|
removeTouchAction(touchArea, index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ActionIcon icon={XIcon} title={t('delete')} />
|
||||||
|
</Popconfirm>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Switch } from 'antd';
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
export default memo(() => {
|
||||||
|
const [enable, updateAgentConfig] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentItem(s)?.touch?.enable,
|
||||||
|
s.updateAgentConfig,
|
||||||
|
]);
|
||||||
|
return (
|
||||||
|
<Switch
|
||||||
|
value={enable}
|
||||||
|
// style={{ width: 48 }}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateAgentConfig({ touch: { enable: value } });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,62 @@
|
|||||||
|
import { ActionIcon } from '@lobehub/ui';
|
||||||
|
import { message } from 'antd';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { Loader2, PlayIcon } from 'lucide-react';
|
||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { speakCharacter } from '@/libs/messages/speakCharacter';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
import { useGlobalStore } from '@/store/global';
|
||||||
|
import { TouchAction } from '@/types/touch';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
touchAction: TouchAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo((props: Props) => {
|
||||||
|
const { touchAction } = props;
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const viewer = useGlobalStore((s) => s.viewer);
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
const currentAgentTTS = useAgentStore((s) => agentSelectors.currentAgentTTS(s), isEqual);
|
||||||
|
|
||||||
|
if (!touchAction) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionIcon
|
||||||
|
icon={loading ? Loader2 : PlayIcon}
|
||||||
|
spin={loading}
|
||||||
|
disable={loading}
|
||||||
|
title={t('play', { ns: 'common' })}
|
||||||
|
key="play"
|
||||||
|
onClick={() => {
|
||||||
|
speakCharacter(
|
||||||
|
{
|
||||||
|
expression: touchAction.expression,
|
||||||
|
tts: {
|
||||||
|
...currentAgentTTS,
|
||||||
|
message: touchAction.text,
|
||||||
|
},
|
||||||
|
motion: touchAction.motion,
|
||||||
|
},
|
||||||
|
viewer,
|
||||||
|
{
|
||||||
|
onStart: () => {
|
||||||
|
setLoading(true);
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
setLoading(false);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
message.error(t('ttsTransformFailed', { ns: 'error' }));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,92 @@
|
|||||||
|
import { Empty } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { get } from 'lodash-es';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import ListItem from '@/components/ListItem';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
import { TouchAction, TouchAreaEnum } from '@/types/touch';
|
||||||
|
|
||||||
|
import Header from '../components/Header';
|
||||||
|
import AddOrEdit from './Actions/AddOrEdit';
|
||||||
|
import Delete from './Actions/Delete';
|
||||||
|
import Play from './Actions/Play';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token }) => ({
|
||||||
|
list: css`
|
||||||
|
width: 100%;
|
||||||
|
`,
|
||||||
|
|
||||||
|
listItem: css`
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
margin-block: 2px;
|
||||||
|
|
||||||
|
font-size: ${token.fontSize}px;
|
||||||
|
|
||||||
|
background-color: ${token.colorBgContainer};
|
||||||
|
border-radius: ${token.borderRadius}px;
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface AreaListProps {
|
||||||
|
areaOptions?: { label: string; value: TouchAreaEnum }[];
|
||||||
|
className?: string;
|
||||||
|
currentTouchArea: TouchAreaEnum;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AreaList = (props: AreaListProps) => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const { currentTouchArea, style, className, areaOptions = [] } = props;
|
||||||
|
const [currentAgentTouch] = useAgentStore((s) => [agentSelectors.currentAgentTouch(s)]);
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
const items = get(currentAgentTouch, currentTouchArea)
|
||||||
|
? (get(currentAgentTouch, currentTouchArea) as TouchAction[])
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const touchArea = areaOptions.find((item) => item.value === currentTouchArea)?.label;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox flex={1} style={style} className={className}>
|
||||||
|
<Header
|
||||||
|
title={t('touch.touchActionList', { touchArea })}
|
||||||
|
extra={<AddOrEdit isEdit={false} touchArea={currentTouchArea} />}
|
||||||
|
/>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
key={`${item.text}_${index}`}
|
||||||
|
className={classNames(styles.listItem)}
|
||||||
|
showAction={true}
|
||||||
|
avatar={<Play key={`${currentTouchArea}_play_${index}`} touchAction={item} />}
|
||||||
|
title={item.text}
|
||||||
|
active={false}
|
||||||
|
actions={[
|
||||||
|
<AddOrEdit
|
||||||
|
key={`${currentTouchArea}_edit_${index}`}
|
||||||
|
index={index}
|
||||||
|
touchArea={currentTouchArea}
|
||||||
|
touchAction={item}
|
||||||
|
isEdit={true}
|
||||||
|
/>,
|
||||||
|
<Delete
|
||||||
|
key={`${currentTouchArea}_delete_${index}`}
|
||||||
|
index={index}
|
||||||
|
touchArea={currentTouchArea}
|
||||||
|
/>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{items.length === 0 && (
|
||||||
|
<Empty description={t('touch.noTouchActions')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AreaList;
|
@ -0,0 +1,60 @@
|
|||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { MousePointerClick } from 'lucide-react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import ListItem from '@/components/ListItem';
|
||||||
|
import { TouchAreaEnum } from '@/types/touch';
|
||||||
|
|
||||||
|
import Enabled from '../ActionList/Actions/Enabled';
|
||||||
|
import Header from '../components/Header';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token, responsive }) => ({
|
||||||
|
listItem: css`
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
margin-block: 2px;
|
||||||
|
border-radius: ${token.borderRadius}px;
|
||||||
|
`,
|
||||||
|
container: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 180px;
|
||||||
|
${responsive.lg} {
|
||||||
|
flex-flow: row wrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IndexProps {
|
||||||
|
areaOptions: { label: string; value: TouchAreaEnum }[];
|
||||||
|
currentTouchArea: TouchAreaEnum;
|
||||||
|
setCurrentTouchArea: (area: TouchAreaEnum) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Index = (props: IndexProps) => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const { currentTouchArea, setCurrentTouchArea, areaOptions = [] } = props;
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox className={styles.container}>
|
||||||
|
<Header title={t('touch.customEnable')} extra={<Enabled />} />
|
||||||
|
{areaOptions.map((item) => (
|
||||||
|
<ListItem
|
||||||
|
avatar={<MousePointerClick />}
|
||||||
|
className={classNames(styles.listItem)}
|
||||||
|
active={item.value === currentTouchArea}
|
||||||
|
key={item.value}
|
||||||
|
title={item.label}
|
||||||
|
onClick={() => setCurrentTouchArea(item.value)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Index;
|
@ -0,0 +1,68 @@
|
|||||||
|
import { InboxOutlined } from '@ant-design/icons';
|
||||||
|
import { Upload } from 'antd';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import AgentViewer from '@/features/AgentViewer';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
import { useGlobalStore } from '@/store/global';
|
||||||
|
import { getModelPathByAgentId } from '@/utils/file';
|
||||||
|
import { cacheStorage } from '@/utils/storage';
|
||||||
|
|
||||||
|
import { useStyles } from './style';
|
||||||
|
|
||||||
|
interface ViewerWithUploadProps {
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ViewerWithUpload = memo<ViewerWithUploadProps>(({ style }) => {
|
||||||
|
const viewer = useGlobalStore((s) => s.viewer);
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
const { styles } = useStyles();
|
||||||
|
|
||||||
|
const [currentAgentId, currentAgent3DModel, updateAgentConfig] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentId(s),
|
||||||
|
agentSelectors.currentAgent3DModel(s),
|
||||||
|
s.updateAgentConfig,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleUploadAvatar = (file: any) => {
|
||||||
|
if (!currentAgentId) return;
|
||||||
|
const blob = new Blob([file], { type: 'application/octet-stream' });
|
||||||
|
const modelKey = getModelPathByAgentId(currentAgentId!);
|
||||||
|
|
||||||
|
cacheStorage.setItem(modelKey, blob).then(() => {
|
||||||
|
updateAgentConfig({ meta: { model: modelKey } });
|
||||||
|
const vrmUrl = window.URL.createObjectURL(blob as Blob);
|
||||||
|
viewer.loadVrm(vrmUrl);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return currentAgent3DModel && currentAgentId ? (
|
||||||
|
<AgentViewer agentId={currentAgentId} interactive={false} toolbar={false} />
|
||||||
|
) : (
|
||||||
|
<Upload
|
||||||
|
beforeUpload={handleUploadAvatar}
|
||||||
|
itemRender={() => void 0}
|
||||||
|
accept={'.vrm'}
|
||||||
|
maxCount={1}
|
||||||
|
style={style}
|
||||||
|
openFileDialogOnClick={!currentAgent3DModel}
|
||||||
|
>
|
||||||
|
<Flexbox
|
||||||
|
className={styles.guide}
|
||||||
|
align="center"
|
||||||
|
justify={'center'}
|
||||||
|
width={'100%'}
|
||||||
|
height={'100%'}
|
||||||
|
>
|
||||||
|
<InboxOutlined className={styles.icon} />
|
||||||
|
<p className={styles.info}>{t('uploadTip', { ns: 'common' })}</p>
|
||||||
|
<p className={styles.extra}>{t('upload.support')}</p>
|
||||||
|
</Flexbox>
|
||||||
|
</Upload>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ViewerWithUpload;
|
@ -0,0 +1,24 @@
|
|||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
|
||||||
|
import { ROLE_VIEWER_WIDTH } from '@/constants/common';
|
||||||
|
|
||||||
|
export const useStyles = createStyles(({ css, token }) => ({
|
||||||
|
guide: css`
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
width: ${ROLE_VIEWER_WIDTH}px;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 480px;
|
||||||
|
|
||||||
|
border: 1px dashed ${token.colorBorderSecondary};
|
||||||
|
`,
|
||||||
|
icon: css`
|
||||||
|
font-size: 48px;
|
||||||
|
color: ${token.geekblue};
|
||||||
|
`,
|
||||||
|
info: css``,
|
||||||
|
extra: css`
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${token.colorTextDescription};
|
||||||
|
`,
|
||||||
|
}));
|
@ -0,0 +1,32 @@
|
|||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import React from 'react';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
extra?: React.ReactNode;
|
||||||
|
title: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token }) => ({
|
||||||
|
title: css`
|
||||||
|
font-size: ${token.fontSize}px;
|
||||||
|
color: ${token.colorPrimary};
|
||||||
|
`,
|
||||||
|
|
||||||
|
header: css`
|
||||||
|
align-items: center;
|
||||||
|
height: 48px;
|
||||||
|
font-size: ${token.fontSize}px;
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default (props: HeaderProps) => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const { title, extra } = props;
|
||||||
|
return (
|
||||||
|
<Flexbox justify="space-between" horizontal className={styles.header}>
|
||||||
|
<div className={styles.title}>{title}</div>
|
||||||
|
{extra}
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,95 @@
|
|||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { memo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import { ROLE_VIEWER_WIDTH } from '@/constants/common';
|
||||||
|
import { TouchAreaEnum } from '@/types/touch';
|
||||||
|
|
||||||
|
import ActionList from './ActionList';
|
||||||
|
import SideBar from './SideBar';
|
||||||
|
import ViewerWithUpload from './ViewerWithUpload';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token, responsive }) => ({
|
||||||
|
container: css`
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
min-height: 480px;
|
||||||
|
padding: 0 16px;
|
||||||
|
|
||||||
|
background-color: rgba(255, 255, 255, 2%);
|
||||||
|
border-radius: ${token.borderRadius}px;
|
||||||
|
|
||||||
|
${responsive.lg} {
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
model: css`
|
||||||
|
width: ${ROLE_VIEWER_WIDTH}px;
|
||||||
|
height: 100%;
|
||||||
|
${responsive.lg} {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface TouchProps {
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Touch = (props: TouchProps) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const [currentTouchArea, setCurrentTouchArea] = useState<TouchAreaEnum>(TouchAreaEnum.Head);
|
||||||
|
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
const TOUCH_AREA_OPTIONS = [
|
||||||
|
{
|
||||||
|
label: t('touch.area.head'),
|
||||||
|
value: TouchAreaEnum.Head,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.area.arm'),
|
||||||
|
value: TouchAreaEnum.Arm,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.area.leg'),
|
||||||
|
value: TouchAreaEnum.Leg,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.area.chest'),
|
||||||
|
value: TouchAreaEnum.Chest,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.area.belly'),
|
||||||
|
value: TouchAreaEnum.Belly,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('touch.area.buttocks'),
|
||||||
|
value: TouchAreaEnum.Buttocks,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox className={classNames(className, styles.container)} style={style} horizontal gap={12}>
|
||||||
|
<SideBar
|
||||||
|
currentTouchArea={currentTouchArea}
|
||||||
|
setCurrentTouchArea={setCurrentTouchArea}
|
||||||
|
areaOptions={TOUCH_AREA_OPTIONS}
|
||||||
|
/>
|
||||||
|
<ActionList currentTouchArea={currentTouchArea} areaOptions={TOUCH_AREA_OPTIONS} />
|
||||||
|
<Flexbox className={styles.model}>
|
||||||
|
<ViewerWithUpload />
|
||||||
|
</Flexbox>
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Touch);
|
@ -0,0 +1,35 @@
|
|||||||
|
import { Select } from 'antd';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
import { TTS_ENGINE } from '@/types/tts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const [engine, updateAgentTTS] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentTTS(s)?.engine,
|
||||||
|
s.updateAgentTTS,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
value={engine}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: 'Edge',
|
||||||
|
value: 'edge',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateAgentTTS({ engine: value as TTS_ENGINE });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,30 @@
|
|||||||
|
import { Select } from 'antd';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
|
||||||
|
import { supportedLocales } from '@/constants/tts';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const [locale, updateAgentTTS] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentTTS(s)?.locale,
|
||||||
|
s.updateAgentTTS,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
value={locale}
|
||||||
|
options={supportedLocales}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateAgentTTS({ locale: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,44 @@
|
|||||||
|
import { InputNumber, Slider } from 'antd';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import { MAX_TTS_PITCH, MIN_TTS_PITCH, TTS_PITCH_STEP } from '@/constants/tts';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const [pitch, updateAgentTTS] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentTTS(s)?.pitch,
|
||||||
|
s.updateAgentTTS,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox className={className} style={style} flex={1} horizontal gap={8}>
|
||||||
|
<Slider
|
||||||
|
value={pitch}
|
||||||
|
max={MAX_TTS_PITCH}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
min={MIN_TTS_PITCH}
|
||||||
|
step={TTS_PITCH_STEP}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateAgentTTS({ pitch: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
min={MIN_TTS_PITCH}
|
||||||
|
max={MAX_TTS_PITCH}
|
||||||
|
step={TTS_PITCH_STEP}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
value={pitch}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateAgentTTS({ pitch: value === null ? undefined : value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,72 @@
|
|||||||
|
import { PlayCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { useRequest } from 'ahooks';
|
||||||
|
import { Button, message } from 'antd';
|
||||||
|
import React, { CSSProperties, memo, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { supportedLocales } from '@/constants/tts';
|
||||||
|
import { speechApi } from '@/services/tts';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const ref = useRef<HTMLAudioElement>(null);
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
const tts = useAgentStore((s) => agentSelectors.currentAgentTTS(s));
|
||||||
|
const sample = supportedLocales.find((item) => item.value === tts?.locale)?.sample;
|
||||||
|
|
||||||
|
const { loading, run: speek } = useRequest(speechApi, {
|
||||||
|
manual: true,
|
||||||
|
onError: (err) => {
|
||||||
|
message.error(err.message);
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.pause();
|
||||||
|
ref.current.currentTime = 0;
|
||||||
|
ref.current.src = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: (res) => {
|
||||||
|
message.success(t('tts.transformSuccess'));
|
||||||
|
const adUrl = URL.createObjectURL(new Blob([res]));
|
||||||
|
if (ref.current) {
|
||||||
|
ref.current.src = adUrl;
|
||||||
|
ref.current.play();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
htmlType="button"
|
||||||
|
type={'primary'}
|
||||||
|
style={style}
|
||||||
|
className={className}
|
||||||
|
icon={<PlayCircleOutlined />}
|
||||||
|
loading={loading}
|
||||||
|
onClick={() => {
|
||||||
|
if (!tts?.locale) {
|
||||||
|
message.error(t('tts.selectLanguage'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!tts?.voice) {
|
||||||
|
message.error(t('tts.selectVoice'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (sample) {
|
||||||
|
speek({ ...tts, message: sample });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('tts.audition')}
|
||||||
|
</Button>
|
||||||
|
<audio ref={ref} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,44 @@
|
|||||||
|
import { InputNumber, Slider } from 'antd';
|
||||||
|
import React, { CSSProperties, memo } from 'react';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import { MAX_TTS_SPEED, MIN_TTS_SPEED, TTS_SPEED_STEP } from '@/constants/tts';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const [speed, updateAgentTTS] = useAgentStore((s) => [
|
||||||
|
agentSelectors.currentAgentTTS(s)?.speed,
|
||||||
|
s.updateAgentTTS,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox className={className} style={style} flex={1} horizontal gap={8}>
|
||||||
|
<Slider
|
||||||
|
value={speed}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
min={MIN_TTS_SPEED}
|
||||||
|
max={MAX_TTS_SPEED}
|
||||||
|
step={TTS_SPEED_STEP}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateAgentTTS({ speed: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<InputNumber
|
||||||
|
min={MIN_TTS_SPEED}
|
||||||
|
max={MAX_TTS_SPEED}
|
||||||
|
step={TTS_SPEED_STEP}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
value={speed}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateAgentTTS({ speed: value === null ? undefined : value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,72 @@
|
|||||||
|
import { useRequest } from 'ahooks';
|
||||||
|
import { Select } from 'antd';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import React, { CSSProperties, memo, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { voiceListApi } from '@/services/tts';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
import { Voice } from '@/types/tts';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo<Props>((props) => {
|
||||||
|
const { style, className } = props;
|
||||||
|
const [voices, setVoices] = useState<Voice[]>([]);
|
||||||
|
|
||||||
|
const [voice, engine, locale, updateAgentTTS] = useAgentStore(
|
||||||
|
(s) => [
|
||||||
|
agentSelectors.currentAgentTTS(s)?.voice,
|
||||||
|
agentSelectors.currentAgentTTS(s)?.engine,
|
||||||
|
agentSelectors.currentAgentTTS(s)?.locale,
|
||||||
|
s.updateAgentTTS,
|
||||||
|
],
|
||||||
|
isEqual,
|
||||||
|
);
|
||||||
|
|
||||||
|
const { loading: voiceLoading } = useRequest(
|
||||||
|
() => {
|
||||||
|
if (!engine) {
|
||||||
|
return Promise.resolve({ data: [] });
|
||||||
|
}
|
||||||
|
return voiceListApi(engine);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: (res) => {
|
||||||
|
setVoices(res.data);
|
||||||
|
},
|
||||||
|
refreshDeps: [engine],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!locale) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const voice = voices.find((voice) => voice.locale === locale);
|
||||||
|
if (voice) {
|
||||||
|
updateAgentTTS({ voice: voice.ShortName });
|
||||||
|
}
|
||||||
|
}, [locale, engine]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
value={voice}
|
||||||
|
disabled={voiceLoading}
|
||||||
|
loading={voiceLoading}
|
||||||
|
options={voices
|
||||||
|
.filter((voice) => voice.locale === locale)
|
||||||
|
.map((item) => ({
|
||||||
|
label: `${item.DisplayName}-${item.LocalName}`,
|
||||||
|
value: item.ShortName,
|
||||||
|
}))}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateAgentTTS({ voice: value });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,55 @@
|
|||||||
|
import { Form, FormProps } from '@lobehub/ui';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { FORM_STYLE } from '@/constants/token';
|
||||||
|
|
||||||
|
import TTSEngine from './TTSEngine';
|
||||||
|
import TTSLocale from './TTSLocale';
|
||||||
|
import TTSPitch from './TTSPitch';
|
||||||
|
import TTSPlay from './TTSPlay';
|
||||||
|
import TTSSpeed from './TTSSpeed';
|
||||||
|
import TTSVoice from './TTSVoice';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
const voice: FormProps['items'] = [
|
||||||
|
{
|
||||||
|
label: t('tts.engineLabel'),
|
||||||
|
desc: t('tts.engineDescription'),
|
||||||
|
name: 'engine',
|
||||||
|
children: <TTSEngine />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('tts.localeLabel'),
|
||||||
|
desc: t('tts.localeDescription'),
|
||||||
|
name: 'locale',
|
||||||
|
children: <TTSLocale />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('tts.voiceLabel'),
|
||||||
|
desc: t('tts.voiceDescription'),
|
||||||
|
name: 'voice',
|
||||||
|
children: <TTSVoice />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('tts.speedLabel'),
|
||||||
|
desc: t('tts.speedDescription'),
|
||||||
|
name: 'speed',
|
||||||
|
children: <TTSSpeed />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('tts.pitchLabel'),
|
||||||
|
desc: t('tts.pitchDescription'),
|
||||||
|
name: 'pitch',
|
||||||
|
children: <TTSPitch />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('tts.audition'),
|
||||||
|
desc: t('tts.auditionDescription'),
|
||||||
|
children: <TTSPlay />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return <Form items={voice} itemsType={'flat'} variant={'block'} {...FORM_STYLE} />;
|
||||||
|
};
|
@ -0,0 +1,29 @@
|
|||||||
|
import { ActionIcon, Icon } from '@lobehub/ui';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { Book } from 'lucide-react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { DESKTOP_HEADER_ICON_SIZE } from '@/constants/token';
|
||||||
|
|
||||||
|
const handleOpenDocs = () => {
|
||||||
|
window.open('https://docs.vidol.chat/role-manual/quickstart/introduction', '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const RoleBookButton = memo<{ modal?: boolean }>(({ modal }) => {
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
return modal ? (
|
||||||
|
<Button icon={<Icon icon={Book} />} onClick={handleOpenDocs}>
|
||||||
|
{t('roleBook')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<ActionIcon
|
||||||
|
icon={Book}
|
||||||
|
onClick={handleOpenDocs}
|
||||||
|
size={DESKTOP_HEADER_ICON_SIZE}
|
||||||
|
title={t('roleBook')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default RoleBookButton;
|
@ -0,0 +1,176 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Alert, Icon, Modal, type ModalProps } from '@lobehub/ui';
|
||||||
|
import { Button, Divider, Input, Popover, Progress, Space, Typography, message } from 'antd';
|
||||||
|
import { useTheme } from 'antd-style';
|
||||||
|
import isEqual from 'fast-deep-equal';
|
||||||
|
import { kebabCase } from 'lodash-es';
|
||||||
|
import { Dices } from 'lucide-react';
|
||||||
|
import qs from 'query-string';
|
||||||
|
import React, { memo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import AgentCard from '@/components/agent/AgentCard';
|
||||||
|
import SystemRole from '@/components/agent/SystemRole';
|
||||||
|
import { AGENTS_INDEX_GITHUB_ISSUE } from '@/constants/url';
|
||||||
|
import { useUploadAgent } from '@/hooks/useUploadAgent';
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
import { configSelectors, useSettingStore } from '@/store/setting';
|
||||||
|
import { Agent } from '@/types/agent';
|
||||||
|
|
||||||
|
const SubmitAgentModal = memo<ModalProps>(({ open, onCancel }) => {
|
||||||
|
const [agentId, setAgentId] = useState('');
|
||||||
|
const theme = useTheme();
|
||||||
|
const currentAgent: Agent | undefined = useAgentStore(
|
||||||
|
(s) => agentSelectors.currentAgentItem(s),
|
||||||
|
isEqual,
|
||||||
|
);
|
||||||
|
const language = useSettingStore((s) => configSelectors.currentLanguage(s));
|
||||||
|
|
||||||
|
const meta = currentAgent?.meta;
|
||||||
|
const { t } = useTranslation(['role', 'common', 'error']);
|
||||||
|
|
||||||
|
const { uploading, uploadAgentData, percent } = useUploadAgent();
|
||||||
|
|
||||||
|
const isFormPass = Boolean(
|
||||||
|
currentAgent?.greeting &&
|
||||||
|
currentAgent?.systemRole &&
|
||||||
|
meta?.name &&
|
||||||
|
meta?.description &&
|
||||||
|
meta?.avatar &&
|
||||||
|
meta?.model,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!currentAgent || !meta || !agentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { avatarUrl, coverUrl, modelUrl } = await uploadAgentData(agentId, meta);
|
||||||
|
if (!avatarUrl || !coverUrl || !modelUrl) {
|
||||||
|
message.error(t('fileUploadError', { ns: 'error' }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = [
|
||||||
|
'### agentId',
|
||||||
|
agentId,
|
||||||
|
'### avatar',
|
||||||
|
avatarUrl,
|
||||||
|
'### cover',
|
||||||
|
coverUrl,
|
||||||
|
'### systemRole',
|
||||||
|
currentAgent.systemRole,
|
||||||
|
'### greeting',
|
||||||
|
currentAgent.greeting,
|
||||||
|
'### modelUrl',
|
||||||
|
modelUrl,
|
||||||
|
'### name',
|
||||||
|
meta.name,
|
||||||
|
'### description',
|
||||||
|
meta.description,
|
||||||
|
'### category',
|
||||||
|
meta.category,
|
||||||
|
'### readme',
|
||||||
|
meta.readme,
|
||||||
|
'### gender',
|
||||||
|
meta.gender,
|
||||||
|
'### tts',
|
||||||
|
JSON.stringify(currentAgent.tts),
|
||||||
|
'### touch',
|
||||||
|
JSON.stringify(currentAgent.touch),
|
||||||
|
'### model',
|
||||||
|
currentAgent.model,
|
||||||
|
'### params',
|
||||||
|
JSON.stringify(currentAgent.params),
|
||||||
|
'### locale',
|
||||||
|
language,
|
||||||
|
].join('\n\n');
|
||||||
|
|
||||||
|
const url = qs.stringifyUrl({
|
||||||
|
query: { body, labels: '🤖 Agent PR', title: `[Agent] ${meta.name}` },
|
||||||
|
url: AGENTS_INDEX_GITHUB_ISSUE,
|
||||||
|
});
|
||||||
|
|
||||||
|
window.open(url, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
allowFullscreen
|
||||||
|
footer={
|
||||||
|
<Popover
|
||||||
|
open={uploading}
|
||||||
|
title={
|
||||||
|
<Flexbox>
|
||||||
|
<Typography.Text type={'secondary'}>{t('submit.uploadingTip')}</Typography.Text>
|
||||||
|
<Space>
|
||||||
|
<Progress steps={30} percent={percent.cover} size="small" />
|
||||||
|
<Typography.Text style={{ fontSize: 12 }}>
|
||||||
|
{t('submit.uploadingCover')}
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Progress steps={30} percent={percent.avatar} size="small" />
|
||||||
|
<Typography.Text style={{ fontSize: 12 }}>
|
||||||
|
{t('submit.uploadingAvatar')}
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
<Space>
|
||||||
|
<Progress steps={30} percent={percent.model} size="small" />
|
||||||
|
<Typography.Text style={{ fontSize: 12 }}>
|
||||||
|
{t('submit.uploadingModel')}
|
||||||
|
</Typography.Text>
|
||||||
|
</Space>
|
||||||
|
</Flexbox>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
disabled={!isFormPass || !agentId}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
size={'large'}
|
||||||
|
type={'primary'}
|
||||||
|
loading={uploading}
|
||||||
|
>
|
||||||
|
{t('submit.submitAssistant')}
|
||||||
|
</Button>
|
||||||
|
</Popover>
|
||||||
|
}
|
||||||
|
onCancel={onCancel}
|
||||||
|
open={open}
|
||||||
|
title={t('shareToMarket')}
|
||||||
|
>
|
||||||
|
<Flexbox gap={16}>
|
||||||
|
{!isFormPass && <Alert message={t('submit.submitWarning')} showIcon type={'warning'} />}
|
||||||
|
<AgentCard agent={currentAgent} />
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
<SystemRole systemRole={currentAgent?.systemRole} />
|
||||||
|
<Divider style={{ margin: '8px 0' }} />
|
||||||
|
<strong>
|
||||||
|
<span style={{ color: theme.colorError, marginRight: 4 }}>*</span>
|
||||||
|
agentId {t('submit.assistantId')}
|
||||||
|
</strong>
|
||||||
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
|
<Input
|
||||||
|
onChange={(e) => setAgentId(e.target.value)}
|
||||||
|
placeholder={t('submit.assistantIdTip')}
|
||||||
|
value={agentId}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<Icon icon={Dices} />}
|
||||||
|
title={t('random', { ns: 'common' })}
|
||||||
|
onClick={() => {
|
||||||
|
const randomId = Math.random().toString(36).slice(7);
|
||||||
|
setAgentId(kebabCase(randomId));
|
||||||
|
}}
|
||||||
|
></Button>
|
||||||
|
</Space.Compact>
|
||||||
|
</Flexbox>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SubmitAgentModal;
|
@ -0,0 +1,34 @@
|
|||||||
|
import { ActionIcon, Icon } from '@lobehub/ui';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { Share2 } from 'lucide-react';
|
||||||
|
import { memo, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { DESKTOP_HEADER_ICON_SIZE } from '@/constants/token';
|
||||||
|
|
||||||
|
import SubmitAgentModal from './SubmitAgentModal';
|
||||||
|
|
||||||
|
const SubmitAgentButton = memo<{ modal?: boolean }>(({ modal }) => {
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
const shareToMarket = t('shareToMarket');
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{modal ? (
|
||||||
|
<Button block icon={<Icon icon={Share2} />} onClick={() => setIsModalOpen(true)}>
|
||||||
|
{shareToMarket}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<ActionIcon
|
||||||
|
icon={Share2}
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
size={DESKTOP_HEADER_ICON_SIZE}
|
||||||
|
title={shareToMarket}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SubmitAgentModal onCancel={() => setIsModalOpen(false)} open={isModalOpen} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SubmitAgentButton;
|
@ -0,0 +1,46 @@
|
|||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
|
||||||
|
export const useStyles = createStyles(({ css, token, prefixCls }) => ({
|
||||||
|
author: css`
|
||||||
|
font-size: 12px;
|
||||||
|
`,
|
||||||
|
|
||||||
|
avatar: css`
|
||||||
|
flex: none;
|
||||||
|
`,
|
||||||
|
container: css`
|
||||||
|
position: relative;
|
||||||
|
padding: 16px 16px 24px;
|
||||||
|
border-bottom: 1px solid ${token.colorBorderSecondary};
|
||||||
|
`,
|
||||||
|
date: css`
|
||||||
|
font-size: 12px;
|
||||||
|
color: ${token.colorTextDescription};
|
||||||
|
`,
|
||||||
|
desc: css`
|
||||||
|
color: ${token.colorTextDescription};
|
||||||
|
text-align: center;
|
||||||
|
`,
|
||||||
|
loading: css`
|
||||||
|
.${prefixCls}-skeleton-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
nav: css`
|
||||||
|
padding-top: 4px;
|
||||||
|
|
||||||
|
.${prefixCls}-tabs-tab {
|
||||||
|
margin: 4px !important;
|
||||||
|
|
||||||
|
+ .${prefixCls}-tabs-tab {
|
||||||
|
margin: 4px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
title: css`
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-align: center;
|
||||||
|
`,
|
||||||
|
}));
|
@ -0,0 +1,20 @@
|
|||||||
|
import { ActionIcon } from '@lobehub/ui';
|
||||||
|
import { ChevronsLeft, List } from 'lucide-react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { DESKTOP_HEADER_ICON_SIZE } from '@/constants/token';
|
||||||
|
import { useGlobalStore } from '@/store/global';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const [showRoleList, toggleRoleList] = useGlobalStore((s) => [s.showRoleList, s.toggleRoleList]);
|
||||||
|
const { t } = useTranslation('chat');
|
||||||
|
return (
|
||||||
|
<ActionIcon
|
||||||
|
icon={showRoleList ? ChevronsLeft : List}
|
||||||
|
onClick={() => toggleRoleList()}
|
||||||
|
title={t('roleList')}
|
||||||
|
size={DESKTOP_HEADER_ICON_SIZE}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,80 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { TabsNav } from '@lobehub/ui';
|
||||||
|
import { useResponsive } from 'ahooks';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import Info from './Info';
|
||||||
|
import LangModel from './LangModel';
|
||||||
|
import Role from './Role';
|
||||||
|
import Shell from './Shell';
|
||||||
|
import Voice from './Voice';
|
||||||
|
import RoleBookButton from './actions/RoleBook';
|
||||||
|
import SubmitAgentButton from './actions/SubmitAgentButton';
|
||||||
|
import ToogleRoleList from './actions/ToogleRoleList';
|
||||||
|
import { useStyles } from './style';
|
||||||
|
|
||||||
|
interface RolePanelProps {
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RolePanel = (props: RolePanelProps) => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const { md = true } = useResponsive();
|
||||||
|
const { className, style } = props;
|
||||||
|
const [tab, setTab] = useState('info');
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox flex={1} gap={12} className={classNames(styles.container, className)} style={style}>
|
||||||
|
<TabsNav
|
||||||
|
activeKey={tab}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: 'info',
|
||||||
|
label: t('nav.info'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'role',
|
||||||
|
label: t('nav.role'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'voice',
|
||||||
|
label: t('nav.voice'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'shell',
|
||||||
|
label: t('nav.shell'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'llm',
|
||||||
|
label: t('nav.llm'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
tabBarExtraContent={{
|
||||||
|
left: <ToogleRoleList />,
|
||||||
|
right: (
|
||||||
|
<Flexbox horizontal gap={8}>
|
||||||
|
<RoleBookButton modal={md} />
|
||||||
|
<SubmitAgentButton modal={md} />
|
||||||
|
</Flexbox>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
onChange={(key) => {
|
||||||
|
setTab(key);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{tab === 'info' ? <Info /> : null}
|
||||||
|
{tab === 'role' ? <Role /> : null}
|
||||||
|
{tab === 'voice' ? <Voice /> : null}
|
||||||
|
{tab === 'shell' ? <Shell /> : null}
|
||||||
|
{tab === 'llm' ? <LangModel /> : null}
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RolePanel;
|
@ -0,0 +1,7 @@
|
|||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
|
||||||
|
export const useStyles = createStyles(({ css }) => ({
|
||||||
|
container: css`
|
||||||
|
width: 100%;
|
||||||
|
`,
|
||||||
|
}));
|
@ -0,0 +1,25 @@
|
|||||||
|
import { FormInstance } from 'antd/es/form/hooks/useForm';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { useLayoutEffect } from 'react';
|
||||||
|
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
export const useSyncSettings = (form: FormInstance) => {
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const currentAgent = agentSelectors.currentAgentItem(useAgentStore.getState());
|
||||||
|
form.setFieldsValue(currentAgent);
|
||||||
|
|
||||||
|
// sync with later updated settings
|
||||||
|
const unsubscribe = useAgentStore.subscribe(
|
||||||
|
(s) => agentSelectors.currentAgentItem(s),
|
||||||
|
(agent) => {
|
||||||
|
form.setFieldsValue(agent);
|
||||||
|
},
|
||||||
|
{ equalityFn: isEqual },
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
@ -0,0 +1,128 @@
|
|||||||
|
import { ActionIcon, Modal } from '@lobehub/ui';
|
||||||
|
import { Avatar, Button, message } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import { MessageSquarePlus } from 'lucide-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import ListItem from '@/components/ListItem';
|
||||||
|
import { useAgentStore } from '@/store/agent';
|
||||||
|
import { GenderEnum } from '@/types/agent';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token }) => ({
|
||||||
|
genderList: css`
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 24px;
|
||||||
|
`,
|
||||||
|
genderCard: css`
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
border: 1px solid ${token.colorBorder};
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: ${token.colorPrimary};
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-color: ${token.colorPrimary};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
createButton: css`
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 24px;
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export default function CreateRole() {
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [selectedGender, setSelectedGender] = useState<GenderEnum | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
const createNewAgent = useAgentStore((s) => s.createNewAgent);
|
||||||
|
|
||||||
|
const handleCreateRole = async () => {
|
||||||
|
if (!selectedGender) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await createNewAgent(selectedGender);
|
||||||
|
router.push(`/role`);
|
||||||
|
setIsModalOpen(false);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('create role failed:', error);
|
||||||
|
message.error(t('role.createRoleFailed'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const genderOptions = [
|
||||||
|
{
|
||||||
|
key: GenderEnum.MALE,
|
||||||
|
label: t('agent.male'),
|
||||||
|
icon: 'https://oss.vidol.chat/docs/2024/12/8fc3717dd8f190f26cf204789d54b297.png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: GenderEnum.FEMALE,
|
||||||
|
label: t('agent.female'),
|
||||||
|
icon: 'https://oss.vidol.chat/docs/2024/12/6c45a6c800590acdb782ab905939fa04.png',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ActionIcon
|
||||||
|
onClick={() => setIsModalOpen(true)}
|
||||||
|
icon={MessageSquarePlus}
|
||||||
|
size="large"
|
||||||
|
title={t('role.create')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
allowFullscreen
|
||||||
|
height={360}
|
||||||
|
title={t('role.selectGender')}
|
||||||
|
open={isModalOpen}
|
||||||
|
footer={
|
||||||
|
<Button
|
||||||
|
className={styles.createButton}
|
||||||
|
type="primary"
|
||||||
|
disabled={!selectedGender || loading}
|
||||||
|
onClick={handleCreateRole}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
{t('role.create')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
width={480}
|
||||||
|
onCancel={() => !loading && setIsModalOpen(false)}
|
||||||
|
closable={!loading}
|
||||||
|
maskClosable={!loading}
|
||||||
|
>
|
||||||
|
<div className={styles.genderList}>
|
||||||
|
{genderOptions.map((option) => (
|
||||||
|
<ListItem
|
||||||
|
key={option.key}
|
||||||
|
avatar={<Avatar src={option.icon} size={120} />}
|
||||||
|
className={`${styles.genderCard} ${selectedGender === option.key ? 'selected' : ''}`}
|
||||||
|
onClick={() => setSelectedGender(option.key as GenderEnum)}
|
||||||
|
active={selectedGender === option.key}
|
||||||
|
title={option.label}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Typography } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import CreateRole from './CreateRole';
|
||||||
|
|
||||||
|
export const useStyles = createStyles(({ css, token }) => ({
|
||||||
|
logo: css`
|
||||||
|
color: ${token.colorText};
|
||||||
|
fill: ${token.colorText};
|
||||||
|
`,
|
||||||
|
top: css`
|
||||||
|
position: sticky;
|
||||||
|
inset-block-start: 0;
|
||||||
|
height: 64px;
|
||||||
|
line-height: 64px;
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const RoleHeader = memo(() => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox className={styles.top} gap={16} padding={12}>
|
||||||
|
<Flexbox distribution={'space-between'} horizontal>
|
||||||
|
<Flexbox align={'center'} gap={4} horizontal>
|
||||||
|
<Typography.Title level={4} style={{ margin: 0 }}>
|
||||||
|
{t('role.myRole')}
|
||||||
|
</Typography.Title>
|
||||||
|
</Flexbox>
|
||||||
|
<CreateRole />
|
||||||
|
</Flexbox>
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default RoleHeader;
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Space, Tag } from 'antd';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { LOBE_VIDOL_DEFAULT_AGENT_ID } from '@/constants/agent';
|
||||||
|
import { useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
import ListItem from '../../ListItem';
|
||||||
|
|
||||||
|
const V = memo(() => {
|
||||||
|
const { t } = useTranslation('common');
|
||||||
|
const [activeId, activateAgent, defaultAgent] = useAgentStore((s) => [
|
||||||
|
s.currentIdentifier,
|
||||||
|
s.activateAgent,
|
||||||
|
s.defaultAgent,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
onClick={() => {
|
||||||
|
activateAgent(LOBE_VIDOL_DEFAULT_AGENT_ID);
|
||||||
|
}}
|
||||||
|
active={activeId === LOBE_VIDOL_DEFAULT_AGENT_ID}
|
||||||
|
avatar={defaultAgent.meta.avatar}
|
||||||
|
title={
|
||||||
|
<Space align={'center'}>
|
||||||
|
{defaultAgent.meta.name}
|
||||||
|
<Tag color="geekblue">{t('defaultAssistant')}</Tag>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
description={defaultAgent.meta.description}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default V;
|
@ -0,0 +1,80 @@
|
|||||||
|
import { ActionIcon } from '@lobehub/ui';
|
||||||
|
import { App, Dropdown, MenuProps } from 'antd';
|
||||||
|
import { MessageCircle, MoreVertical, Trash2 } from 'lucide-react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useAgentStore } from '@/store/agent';
|
||||||
|
import { useSessionStore } from '@/store/session';
|
||||||
|
|
||||||
|
interface ActionsProps {
|
||||||
|
id: string;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props: ActionsProps) => {
|
||||||
|
const { id, setOpen } = props;
|
||||||
|
const { modal } = App.useApp();
|
||||||
|
const router = useRouter();
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
const [removeLocalAgent] = useAgentStore((s) => [s.removeLocalAgent]);
|
||||||
|
const currentAgent = useAgentStore((s) => s.getAgentById(id));
|
||||||
|
const [createSession, removeSessionByAgentId] = useSessionStore((s) => [
|
||||||
|
s.createSession,
|
||||||
|
s.removeSessionByAgentId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
const items: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
icon: <MessageCircle />,
|
||||||
|
label: t('startChat'),
|
||||||
|
key: 'chat',
|
||||||
|
onClick: ({ domEvent }) => {
|
||||||
|
domEvent.stopPropagation();
|
||||||
|
if (!currentAgent) return;
|
||||||
|
createSession(currentAgent);
|
||||||
|
router.push('/chat');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
danger: true,
|
||||||
|
icon: <Trash2 />,
|
||||||
|
key: 'delete',
|
||||||
|
label: t('delRole'),
|
||||||
|
onClick: ({ domEvent }) => {
|
||||||
|
domEvent.stopPropagation();
|
||||||
|
modal.confirm({
|
||||||
|
centered: true,
|
||||||
|
okButtonProps: { danger: true },
|
||||||
|
async onOk() {
|
||||||
|
await removeLocalAgent(id);
|
||||||
|
removeSessionByAgentId(id);
|
||||||
|
},
|
||||||
|
okText: t('delete', { ns: 'common' }),
|
||||||
|
cancelText: t('cancel', { ns: 'common' }),
|
||||||
|
title: t('delAlert'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items,
|
||||||
|
onClick: ({ domEvent }) => {
|
||||||
|
domEvent.stopPropagation();
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onOpenChange={(open) => setOpen(open)}
|
||||||
|
trigger={['click']}
|
||||||
|
>
|
||||||
|
<ActionIcon
|
||||||
|
icon={MoreVertical}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
import { memo, useMemo, useState } from 'react';
|
||||||
|
import { shallow } from 'zustand/shallow';
|
||||||
|
|
||||||
|
import { DEFAULT_AGENT_AVATAR_URL } from '@/constants/common';
|
||||||
|
import { useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
import ListItem from '../../ListItem';
|
||||||
|
import Actions from './Actions';
|
||||||
|
|
||||||
|
interface SessionItemProps {
|
||||||
|
id: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SessionItem = memo<SessionItemProps>(({ id, onClick }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [active] = useAgentStore((s) => [s.currentIdentifier === id]);
|
||||||
|
const [getAgentById] = useAgentStore((s) => [s.getAgentById]);
|
||||||
|
|
||||||
|
const agent = getAgentById(id);
|
||||||
|
const { name, description, avatar } = agent?.meta || {};
|
||||||
|
|
||||||
|
const actions = useMemo(() => <Actions id={id} setOpen={setOpen} />, [id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListItem
|
||||||
|
actions={actions}
|
||||||
|
active={active}
|
||||||
|
avatar={avatar || DEFAULT_AGENT_AVATAR_URL}
|
||||||
|
description={description || agent?.systemRole}
|
||||||
|
onClick={onClick}
|
||||||
|
showAction={open}
|
||||||
|
title={name}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}, shallow);
|
||||||
|
|
||||||
|
export default SessionItem;
|
@ -0,0 +1,44 @@
|
|||||||
|
import { Skeleton } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css }) => ({
|
||||||
|
avatar: css``,
|
||||||
|
paragraph: css`
|
||||||
|
height: 12px !important;
|
||||||
|
margin-top: 12px !important;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
height: 12px !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
title: css`
|
||||||
|
height: 14px !important;
|
||||||
|
margin-top: 4px !important;
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
|
||||||
|
> li {
|
||||||
|
height: 14px !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const SkeletonList = () => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
|
||||||
|
const list = Array.from({ length: 4 }).fill('');
|
||||||
|
return (
|
||||||
|
<Flexbox gap={8} paddingInline={16}>
|
||||||
|
{list.map((_, index) => (
|
||||||
|
<Skeleton
|
||||||
|
active
|
||||||
|
avatar
|
||||||
|
key={index}
|
||||||
|
paragraph={{ className: styles.paragraph, rows: 1 }}
|
||||||
|
title={{ className: styles.title }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SkeletonList;
|
@ -0,0 +1,53 @@
|
|||||||
|
import { Empty } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import LazyLoad from 'react-lazy-load';
|
||||||
|
|
||||||
|
import { agentSelectors, useAgentStore } from '@/store/agent';
|
||||||
|
|
||||||
|
import SessionItem from './Item';
|
||||||
|
|
||||||
|
const useStyles = createStyles(
|
||||||
|
({ css }) => css`
|
||||||
|
min-height: 70px;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
interface SessionListProps {
|
||||||
|
filter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SessionList = memo<SessionListProps>(({ filter }) => {
|
||||||
|
const [filterAgentListIds, activateAgent, agentListIds] = useAgentStore(
|
||||||
|
(s) => [
|
||||||
|
agentSelectors.filterAgentListIds(s, filter),
|
||||||
|
s.activateAgent,
|
||||||
|
agentSelectors.agentListIds(s),
|
||||||
|
],
|
||||||
|
isEqual,
|
||||||
|
);
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{filterAgentListIds.map((id) => (
|
||||||
|
<LazyLoad className={styles} key={id}>
|
||||||
|
<SessionItem
|
||||||
|
id={id}
|
||||||
|
onClick={() => {
|
||||||
|
activateAgent(id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</LazyLoad>
|
||||||
|
))}
|
||||||
|
{agentListIds.length === 0 && (
|
||||||
|
<Empty description={t('noRole')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SessionList;
|
@ -0,0 +1,47 @@
|
|||||||
|
import { Avatar, List, ListItemProps } from '@lobehub/ui';
|
||||||
|
import { useHover } from 'ahooks';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import { memo, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
const { Item } = List;
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token }) => {
|
||||||
|
return {
|
||||||
|
container: css`
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
margin-block: 2px;
|
||||||
|
padding-right: 16px;
|
||||||
|
padding-left: 8px;
|
||||||
|
|
||||||
|
border-radius: ${token.borderRadius}px;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const ListItem = memo<ListItemProps & { avatar: string }>(
|
||||||
|
({ avatar, active, showAction, actions, ...props }) => {
|
||||||
|
const ref = useRef(null);
|
||||||
|
const isHovering = useHover(ref);
|
||||||
|
const { styles } = useStyles();
|
||||||
|
|
||||||
|
const avatarRender = useMemo(
|
||||||
|
() => <Avatar animation={isHovering} avatar={avatar} shape="circle" size={46} />,
|
||||||
|
[isHovering, avatar],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Item
|
||||||
|
actions={actions}
|
||||||
|
active={active}
|
||||||
|
avatar={avatarRender}
|
||||||
|
className={styles.container}
|
||||||
|
ref={ref}
|
||||||
|
showAction={actions && (isHovering || showAction)}
|
||||||
|
{...(props as any)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export default ListItem;
|
@ -0,0 +1,106 @@
|
|||||||
|
import { Icon, SearchBar } from '@lobehub/ui';
|
||||||
|
import { Collapse } from 'antd';
|
||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
import { ChevronDown } from 'lucide-react';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import SkeletonList from '@/components/SkeletonList';
|
||||||
|
|
||||||
|
import Elsa from './List/Elsa';
|
||||||
|
|
||||||
|
const List = dynamic(() => import('./List'), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => <SkeletonList style={{ marginTop: 8 }} />,
|
||||||
|
});
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token, prefixCls }) => ({
|
||||||
|
role: css`
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
|
`,
|
||||||
|
list: css`
|
||||||
|
padding: 8px;
|
||||||
|
`,
|
||||||
|
container: css`
|
||||||
|
.${prefixCls}-collapse-header {
|
||||||
|
padding-inline: 8px !important;
|
||||||
|
color: ${token.colorTextDescription} !important;
|
||||||
|
border-radius: ${token.borderRadius}px !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: ${token.colorText} !important;
|
||||||
|
background: ${token.colorFillTertiary};
|
||||||
|
.${prefixCls}-collapse-extra {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.${prefixCls}-collapse-extra {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.${prefixCls}-collapse-content {
|
||||||
|
border-radius: 0 !important;
|
||||||
|
}
|
||||||
|
.${prefixCls}-collapse-content-box {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
icon: css`
|
||||||
|
transition: all 100ms ${token.motionEaseOut};
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const RoleList = () => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const [searchName, setSearchName] = useState<string>();
|
||||||
|
const { t } = useTranslation('role');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.role}>
|
||||||
|
<Flexbox style={{ padding: '0 8px 0' }} gap={8}>
|
||||||
|
<SearchBar
|
||||||
|
enableShortKey
|
||||||
|
onChange={(e) => {
|
||||||
|
setSearchName(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder={t('search', { ns: 'common' })}
|
||||||
|
shortKey="f"
|
||||||
|
spotlight
|
||||||
|
type={'block'}
|
||||||
|
value={searchName}
|
||||||
|
/>
|
||||||
|
</Flexbox>
|
||||||
|
<div className={styles.list}>
|
||||||
|
<Elsa />
|
||||||
|
<Collapse
|
||||||
|
bordered={false}
|
||||||
|
defaultActiveKey={'default'}
|
||||||
|
className={styles.container}
|
||||||
|
expandIcon={({ isActive }) => (
|
||||||
|
<Icon
|
||||||
|
className={styles.icon}
|
||||||
|
icon={ChevronDown}
|
||||||
|
size={{ fontSize: 16 }}
|
||||||
|
style={isActive ? {} : { rotate: '-90deg' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
expandIconPosition={'end'}
|
||||||
|
ghost
|
||||||
|
size={'small'}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
children: <List filter={searchName} />,
|
||||||
|
label: t('roleList'),
|
||||||
|
key: 'default',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoleList;
|
@ -0,0 +1,62 @@
|
|||||||
|
import { DraggablePanel } from '@lobehub/ui';
|
||||||
|
import { createStyles, useResponsive } from 'antd-style';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { rgba } from 'polished';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { SIDEBAR_MAX_WIDTH, SIDEBAR_WIDTH } from '@/constants/token';
|
||||||
|
import { useGlobalStore } from '@/store/global';
|
||||||
|
|
||||||
|
import RoleHeader from './RoleHeader';
|
||||||
|
import RoleList from './RoleList';
|
||||||
|
|
||||||
|
const useStyles = createStyles(({ css, token }) => ({
|
||||||
|
content: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100% !important;
|
||||||
|
`,
|
||||||
|
sidebar: css`
|
||||||
|
z-index: 10;
|
||||||
|
background-color: ${rgba(token.colorBgLayout, 0.2)};
|
||||||
|
backdrop-filter: saturate(180%) blur(8px);
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const SideBar = () => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
const [showRoleList, setRoleList] = useGlobalStore((s) => [s.showRoleList, s.setRoleList]);
|
||||||
|
|
||||||
|
const { md = true } = useResponsive();
|
||||||
|
|
||||||
|
const [cacheExpand, setCacheExpand] = useState<boolean>(Boolean(showRoleList));
|
||||||
|
|
||||||
|
const handleExpand = (expand: boolean) => {
|
||||||
|
if (isEqual(expand, Boolean(showRoleList))) return;
|
||||||
|
setRoleList(expand);
|
||||||
|
setCacheExpand(expand);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (md && cacheExpand) setRoleList(true);
|
||||||
|
if (!md) setRoleList(false);
|
||||||
|
}, [md, cacheExpand]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DraggablePanel
|
||||||
|
className={styles.sidebar}
|
||||||
|
classNames={{ content: styles.content }}
|
||||||
|
maxWidth={SIDEBAR_MAX_WIDTH}
|
||||||
|
minWidth={SIDEBAR_WIDTH}
|
||||||
|
mode={md ? 'fixed' : 'float'}
|
||||||
|
placement={'left'}
|
||||||
|
onExpandChange={handleExpand}
|
||||||
|
expand={showRoleList}
|
||||||
|
>
|
||||||
|
<RoleHeader />
|
||||||
|
<RoleList />
|
||||||
|
</DraggablePanel>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SideBar;
|
@ -0,0 +1,17 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ReactNode, memo } from 'react';
|
||||||
|
|
||||||
|
import AppLayout from '@/layout/AppLayout';
|
||||||
|
|
||||||
|
export interface LayoutProps {
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LayoutDesktop = (props: LayoutProps) => {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return <AppLayout>{children}</AppLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(LayoutDesktop);
|
@ -0,0 +1,38 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import React, { memo } from 'react';
|
||||||
|
import { Center, Flexbox } from 'react-layout-kit';
|
||||||
|
|
||||||
|
import SideBar from './SideBar';
|
||||||
|
import { useStyles } from './style';
|
||||||
|
|
||||||
|
const RoleEdit = dynamic(() => import('./RoleEdit'), {
|
||||||
|
ssr: false,
|
||||||
|
loading: () => (
|
||||||
|
<Center style={{ height: '100%', width: '100%' }}>
|
||||||
|
<Spin />
|
||||||
|
</Center>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Role = () => {
|
||||||
|
const { styles } = useStyles();
|
||||||
|
return (
|
||||||
|
<Flexbox
|
||||||
|
flex={1}
|
||||||
|
height={'100%'}
|
||||||
|
width={'100%'}
|
||||||
|
horizontal
|
||||||
|
style={{ overflow: 'hidden', position: 'relative' }}
|
||||||
|
>
|
||||||
|
<SideBar />
|
||||||
|
<Flexbox className={styles.preview} horizontal>
|
||||||
|
<RoleEdit />
|
||||||
|
</Flexbox>
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(Role);
|
@ -0,0 +1,18 @@
|
|||||||
|
import { createStyles } from 'antd-style';
|
||||||
|
|
||||||
|
export const useStyles = createStyles(({ css, cx, token }) => ({
|
||||||
|
preview: cx(
|
||||||
|
'role-preview',
|
||||||
|
css`
|
||||||
|
overflow: scroll;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 ${token.paddingSM}px;
|
||||||
|
`,
|
||||||
|
),
|
||||||
|
container: css`
|
||||||
|
width: 1024px;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
`,
|
||||||
|
}));
|
Loading…
Reference in New Issue