Skip to content

Commit

Permalink
✨ feat: 支持上传用户自定义头像
Browse files Browse the repository at this point in the history
  • Loading branch information
rdmclin2 committed May 18, 2024
1 parent 07aa915 commit ac50d24
Show file tree
Hide file tree
Showing 10 changed files with 146 additions and 12 deletions.
3 changes: 3 additions & 0 deletions src/constants/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ export const COOKIE_CACHE_DAYS = 30;
export const LOADING_FLAG = '...';

export const DEFAULT_USER_AVATAR = '😀';

export const DEFAULT_USER_AVATAR_URL =
'https://registry.npmmirror.com/@lobehub/assets-logo/1.2.0/files/assets/logo-3d.webp';
16 changes: 16 additions & 0 deletions src/constants/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DEFAULT_USER_AVATAR } from '@/constants/common';
import { DEFAULT_PRIMARY_COLOR } from '@/constants/theme';
import { Config } from '@/types/config';

export const DEFAULT_SETTINGS: Config = {
backgroundEffect: 'glow',
languageModel: {
openAI: {
apikey: '',
endpoint: '',
model: 'gpt-3.5-turbo',
},
},
primaryColor: DEFAULT_PRIMARY_COLOR,
avatar: DEFAULT_USER_AVATAR,
};
2 changes: 2 additions & 0 deletions src/constants/theme.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const VIDOL_THEME_APPEARANCE = 'VIDOL_THEME_APPEARANCE';
export const VIDOL_THEME_NEUTRAL_COLOR = 'VIDOL_THEME_NEUTRAL_COLOR';
export const VIDOL_THEME_PRIMARY_COLOR = 'VIDOL_THEME_PRIMARY_COLOR';

export const DEFAULT_PRIMARY_COLOR = 'blue';
70 changes: 70 additions & 0 deletions src/features/AvatarWithUpload/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Upload } from 'antd';
import { createStyles } from 'antd-style';
import NextImage from 'next/image';
import { CSSProperties, memo, useCallback } from 'react';

import { DEFAULT_USER_AVATAR_URL } from '@/constants/common';
import { useConfigStore } from '@/store/config';
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 {
compressSize?: number;
id?: string;
size?: number;
style?: CSSProperties;
}

const AvatarWithUpload = memo<AvatarWithUploadProps>(
({ size = 40, compressSize = 256, style, id }) => {
const { styles } = useStyle();
const [avatar, setConfig] = useConfigStore((s) => [s.config.avatar, s.setConfig]);

const handleUploadAvatar = useCallback(
createUploadImageHandler((avatar) => {
const img = new Image();
img.src = avatar;
img.addEventListener('load', () => {
const webpBase64 = imageToBase64({ img, size: compressSize });
setConfig({ avatar: webpBase64 });
});
}),
[],
);

return (
<div className={styles} id={id} style={{ maxHeight: size, maxWidth: size, ...style }}>
<Upload beforeUpload={handleUploadAvatar} itemRender={() => void 0} maxCount={1}>
<NextImage
alt={avatar ? 'userAvatar' : 'LobeChat'}
height={size}
src={!!avatar ? avatar : DEFAULT_USER_AVATAR_URL}
unoptimized
width={size}
/>
</Upload>
</div>
);
},
);

export default AvatarWithUpload;
4 changes: 4 additions & 0 deletions src/features/Settings/common.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import classNames from 'classnames';
import { Monitor, Settings2 } from 'lucide-react';
import React from 'react';

import AvatarWithUpload from '@/features/AvatarWithUpload';
import { useAgentStore } from '@/store/agent';
import { useConfigStore } from '@/store/config';
import { useSessionStore } from '@/store/session';
Expand Down Expand Up @@ -90,6 +91,9 @@ const CommonConfig = (props: CommonConfigProps) => {
<div className={classNames(styles.config, className)} style={style}>
<Form style={{ display: 'flex', flexGrow: 1 }}>
<FormGroup icon={Settings2} title={'主题设置'}>
<FormItem desc={'头像'} divider label={'自定义头像'} name={'avatar'}>
<AvatarWithUpload />
</FormItem>
<FormItem desc={'主题色'} divider label={'自定义主题色'} name={'primaryColor'}>
<Swatches
activeColor={primaryColor}
Expand Down
13 changes: 2 additions & 11 deletions src/store/config/initialState.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DEFAULT_SETTINGS } from '@/constants/settings';
import { INITIAL_COORDINATES } from '@/constants/token';
import { Config, PanelConfig, PanelKey } from '@/types/config';

Expand All @@ -8,17 +9,7 @@ export interface ConfigState {
}

const initialState: ConfigState = {
config: {
backgroundEffect: 'glow',
languageModel: {
openAI: {
apikey: '',
endpoint: '',
model: 'gpt-3.5-turbo',
},
},
primaryColor: 'blue',
},
config: DEFAULT_SETTINGS,
focusList: [],

panel: {
Expand Down
4 changes: 3 additions & 1 deletion src/store/session/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LOBE_VIDOL_DEFAULT_AGENT_ID } from '@/constants/agent';
import { DEFAULT_USER_AVATAR } from '@/constants/common';
import { useAgentStore } from '@/store/agent';
import { useConfigStore } from '@/store/config';
import { Agent } from '@/types/agent';
import { ChatMessage } from '@/types/chat';
import { Session } from '@/types/session';
Expand Down Expand Up @@ -62,10 +63,11 @@ const currentChats = (s: SessionStore): ChatMessage[] => {

const { messages } = session;
return messages?.map((message) => {
const userAvatar = useConfigStore.getState().config.avatar;
return {
...message,
meta: {
avatar: message.role === 'user' ? DEFAULT_USER_AVATAR : avatar,
avatar: message.role === 'user' ? (userAvatar ? userAvatar : DEFAULT_USER_AVATAR) : avatar,
description: message.role === 'user' ? undefined : description,
title: message.role === 'user' ? '你' : name,
},
Expand Down
1 change: 1 addition & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface PanelConfig {
export type PanelKey = keyof PanelConfig;

export interface CommonConfig {
avatar: string;
/**
* 背景类型
*/
Expand Down
8 changes: 8 additions & 0 deletions src/utils/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const createUploadImageHandler =
(onUploadImage: (base64: string) => void) => (file: any) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.addEventListener('load', () => {
onUploadImage(String(reader.result));
});
};
37 changes: 37 additions & 0 deletions src/utils/imageToBase64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export const imageToBase64 = ({
size,
img,
type = 'image/webp',
}: {
img: HTMLImageElement;
size: number;
type?: string;
}) => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
let startX = 0;
let startY = 0;

if (img.width > img.height) {
startX = (img.width - img.height) / 2;
} else {
startY = (img.height - img.width) / 2;
}

canvas.width = size;
canvas.height = size;

ctx.drawImage(
img,
startX,
startY,
Math.min(img.width, img.height),
Math.min(img.width, img.height),
0,
0,
size,
size,
);

return canvas.toDataURL(type);
};

0 comments on commit ac50d24

Please sign in to comment.