feat(setting): avatar setting

This commit is contained in:
kangfenmao 2024-07-05 16:03:13 +08:00
parent 9212b56cdf
commit 5b1eb63066
13 changed files with 216 additions and 12 deletions

View File

@ -27,7 +27,9 @@
"@emotion/styled": "^11.11.5",
"@fontsource/inter": "^5.0.18",
"@reduxjs/toolkit": "^2.2.5",
"ahooks": "^3.8.0",
"antd": "^5.18.3",
"browser-image-compression": "^2.0.2",
"dayjs": "^1.11.11",
"electron-updater": "^6.1.7",
"electron-window-state": "^5.0.3",

View File

@ -7,7 +7,7 @@
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src *; script-src 'self'; style-src 'self' 'unsafe-inline' *; font-src 'self' *; img-src 'self' data:" />
content="default-src 'self'; connect-src *; script-src 'self'; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' *; img-src 'self' data:" />
</head>
<body theme-mode="dark">

View File

@ -2,16 +2,18 @@ import { FC } from 'react'
import Logo from '@renderer/assets/images/logo.png'
import styled from 'styled-components'
import { Link, useLocation } from 'react-router-dom'
import useAvatar from '@renderer/hooks/useAvatar'
const Sidebar: FC = () => {
const { pathname } = useLocation()
const avatar = useAvatar()
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
return (
<Container>
<Avatar>
<AvatarImg src={Logo} />
<AvatarImg src={avatar || Logo} />
</Avatar>
<MainMenus>
<Menus>

View File

@ -0,0 +1,20 @@
import LocalStorage from '@renderer/services/storage'
import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils'
import { useEffect } from 'react'
export function useAppInitEffect() {
const dispatch = useAppDispatch()
useEffect(() => {
runAsyncFunction(async () => {
try {
const storedImage = await LocalStorage.getImage('avatar')
storedImage && dispatch(setAvatar(storedImage))
} catch (error) {
console.error('Error retrieving the image', error)
}
})
}, [dispatch])
}

View File

@ -0,0 +1,5 @@
import { useAppSelector } from '@renderer/store'
export default function useAvatar() {
return useAppSelector((state) => state.runtime.avatar)
}

View File

@ -4,13 +4,14 @@ import { marked } from 'marked'
import { FC } from 'react'
import styled from 'styled-components'
import Logo from '@renderer/assets/images/logo.png'
import useAvatar from '@renderer/hooks/useAvatar'
const MessageItem: FC<{ message: Message }> = ({ message }) => {
const avatar = useAvatar()
return (
<MessageContainer key={message.id}>
<AvatarWrapper>
{message.role === 'assistant' ? <Avatar src={Logo} /> : <Avatar alt="Alice Swift">Y</Avatar>}
</AvatarWrapper>
<AvatarWrapper>{message.role === 'assistant' ? <Avatar src={Logo} /> : <Avatar src={avatar} />}</AvatarWrapper>
<div className="markdown" dangerouslySetInnerHTML={{ __html: marked(message.content) }} />
</MessageContainer>
)

View File

@ -1,11 +1,59 @@
import { FC } from 'react'
import {
SettingContainer,
SettingDivider,
SettingRow,
SettingRowTitle,
SettingTitle
} from './components/SettingComponent'
import { Avatar, message, Upload } from 'antd'
import styled from 'styled-components'
import LocalStorage from '@renderer/services/storage'
import { compressImage } from '@renderer/utils'
import useAvatar from '@renderer/hooks/useAvatar'
import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
const GeneralSettings: FC = () => {
return <Container>General Settings</Container>
const avatar = useAvatar()
const [messageApi, contextHolder] = message.useMessage()
const dispatch = useAppDispatch()
return (
<SettingContainer>
{contextHolder}
<SettingTitle>General Settings</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>Avatar</SettingRowTitle>
<Upload
customRequest={() => {}}
accept="image/png, image/jpeg"
itemRender={() => null}
maxCount={1}
onChange={async ({ file }) => {
try {
const _file = file.originFileObj as File
const compressedFile = await compressImage(_file)
await LocalStorage.storeImage('avatar', compressedFile)
dispatch(setAvatar(await LocalStorage.getImage('avatar')))
} catch (error: any) {
messageApi.open({
type: 'error',
content: error.message
})
}
}}>
<UserAvatar src={avatar} size="large" />
</Upload>
</SettingRow>
<SettingDivider />
</SettingContainer>
)
}
const Container = styled.div`
padding: 20px;
const UserAvatar = styled(Avatar)`
cursor: pointer;
`
export default GeneralSettings

View File

@ -30,3 +30,15 @@ export const SettingSubtitle = styled.div`
export const SettingDivider = styled(Divider)`
margin: 10px 0;
`
export const SettingRow = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`
export const SettingRowTitle = styled.div`
font-size: 14px;
color: var(--color-text-1);
`

View File

@ -1,6 +1,9 @@
import { Topic } from '@renderer/types'
import { convertToBase64 } from '@renderer/utils'
import localforage from 'localforage'
const IMAGE_PREFIX = 'image://'
export default class LocalStorage {
static async getTopic(id: string) {
return localforage.getItem<Topic>(`topic:${id}`)
@ -21,4 +24,23 @@ export default class LocalStorage {
await localforage.setItem(`topic:${id}`, topic)
}
}
static async storeImage(name: string, file: File) {
try {
const base64Image = await convertToBase64(file)
if (typeof base64Image === 'string') {
await localforage.setItem(IMAGE_PREFIX + name, base64Image)
}
} catch (error) {
console.error('Error storing the image', error)
}
}
static async getImage(name: string) {
return localforage.getItem<string>(IMAGE_PREFIX + name)
}
static async removeImage(name: string) {
await localforage.removeItem(IMAGE_PREFIX + name)
}
}

View File

@ -5,18 +5,21 @@ import storage from 'redux-persist/lib/storage'
import assistants from './assistants'
import settings from './settings'
import llm from './llm'
import runtime from './runtime'
const rootReducer = combineReducers({
assistants,
settings,
llm
llm,
runtime
})
const persistedReducer = persistReducer(
{
key: 'cherry-ai',
storage,
version: 1
version: 1,
whitelist: ['runtime']
},
rootReducer
)

View File

@ -0,0 +1,24 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import Logo from '@renderer/assets/images/logo.png'
export interface RuntimeState {
avatar: string
}
const initialState: RuntimeState = {
avatar: Logo
}
const runtimeSlice = createSlice({
name: 'settings',
initialState,
reducers: {
setAvatar: (state, action: PayloadAction<string | null>) => {
state.avatar = action.payload || Logo
}
}
})
export const { setAvatar } = runtimeSlice.actions
export default runtimeSlice.reducer

View File

@ -1,4 +1,5 @@
import { v4 as uuidv4 } from 'uuid'
import imageCompression from 'browser-image-compression'
export const runAsyncFunction = async (fn: () => void) => {
await fn()
@ -48,3 +49,20 @@ export const waitAsyncFunction = (fn: () => Promise<any>, interval = 200, stopTi
}
export const uuid = () => uuidv4()
export const convertToBase64 = (file: File): Promise<string | ArrayBuffer | null> => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onloadend = () => resolve(reader.result)
reader.onerror = reject
reader.readAsDataURL(file)
})
}
export const compressImage = async (file: File) => {
return await imageCompression(file, {
maxSizeMB: 1,
maxWidthOrHeight: 300,
useWebWorker: false
})
}

View File

@ -1424,6 +1424,21 @@ agentkeepalive@^4.2.1:
dependencies:
humanize-ms "^1.2.1"
ahooks@^3.8.0:
version "3.8.0"
resolved "https://registry.yarnpkg.com/ahooks/-/ahooks-3.8.0.tgz#62476bf3459862ff706de2189b87de5e4f49b298"
integrity sha512-M01m+mxLRNNeJ/PCT3Fom26UyreTj6oMqJBetUrJnK4VNI5j6eMA543Xxo53OBXn6XibA2FXKcCCgrT6YCTtKQ==
dependencies:
"@babel/runtime" "^7.21.0"
dayjs "^1.9.1"
intersection-observer "^0.12.0"
js-cookie "^2.x.x"
lodash "^4.17.21"
react-fast-compare "^3.2.2"
resize-observer-polyfill "^1.5.1"
screenfull "^5.0.0"
tslib "^2.4.1"
ajv-keywords@^3.4.1:
version "3.5.2"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
@ -1770,6 +1785,13 @@ braces@^3.0.3, braces@~3.0.2:
dependencies:
fill-range "^7.1.1"
browser-image-compression@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/browser-image-compression/-/browser-image-compression-2.0.2.tgz#4d5ef8882e9e471d6d923715ceb9034499d14eaa"
integrity sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==
dependencies:
uzip "0.20201231.0"
browserslist@^4.22.2:
version "4.23.0"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab"
@ -2125,7 +2147,7 @@ data-view-byte-offset@^1.0.0:
es-errors "^1.3.0"
is-data-view "^1.0.1"
dayjs@^1.11.11:
dayjs@^1.11.11, dayjs@^1.9.1:
version "1.11.11"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.11.tgz#dfe0e9d54c5f8b68ccf8ca5f72ac603e7e5ed59e"
integrity sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==
@ -3271,6 +3293,11 @@ internal-slot@^1.0.7:
hasown "^2.0.0"
side-channel "^1.0.4"
intersection-observer@^0.12.0:
version "0.12.2"
resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.2.tgz#4a45349cc0cd91916682b1f44c28d7ec737dc375"
integrity sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==
is-array-buffer@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98"
@ -3520,6 +3547,11 @@ jake@^10.8.5:
filelist "^1.0.4"
minimatch "^3.1.2"
js-cookie@^2.x.x:
version "2.2.1"
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8"
integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -4543,6 +4575,11 @@ react-dom@^18.2.0:
loose-envify "^1.1.0"
scheduler "^0.23.2"
react-fast-compare@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz#929a97a532304ce9fee4bcae44234f1ce2c21d49"
integrity sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==
react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
@ -4831,6 +4868,11 @@ scheduler@^0.23.2:
dependencies:
loose-envify "^1.1.0"
screenfull@^5.0.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/screenfull/-/screenfull-5.2.0.tgz#6533d524d30621fc1283b9692146f3f13a93d1ba"
integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==
scroll-into-view-if-needed@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz#fa9524518c799b45a2ef6bbffb92bcad0296d01f"
@ -5234,7 +5276,7 @@ tslib@2.6.2, tslib@^2.6.2:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
tslib@^2.1.0:
tslib@^2.1.0, tslib@^2.4.1:
version "2.6.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0"
integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==
@ -5374,6 +5416,11 @@ uuid@^10.0.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-10.0.0.tgz#5a95aa454e6e002725c79055fd42aaba30ca6294"
integrity sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==
uzip@0.20201231.0:
version "0.20201231.0"
resolved "https://registry.yarnpkg.com/uzip/-/uzip-0.20201231.0.tgz#9e64b065b9a8ebf26eb7583fe8e77e1d9a15ed14"
integrity sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==
verror@^1.10.0:
version "1.10.1"
resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.1.tgz#4bf09eeccf4563b109ed4b3d458380c972b0cdeb"