知识库功能开发
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