feat(setting): avatar setting
This commit is contained in:
parent
9212b56cdf
commit
5b1eb63066
@ -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",
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
20
src/renderer/src/hooks/useAppInitEffect.ts
Normal file
20
src/renderer/src/hooks/useAppInitEffect.ts
Normal 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])
|
||||
}
|
||||
5
src/renderer/src/hooks/useAvatar.ts
Normal file
5
src/renderer/src/hooks/useAvatar.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
|
||||
export default function useAvatar() {
|
||||
return useAppSelector((state) => state.runtime.avatar)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
`
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
|
||||
24
src/renderer/src/store/runtime.ts
Normal file
24
src/renderer/src/store/runtime.ts
Normal 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
|
||||
@ -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
|
||||
})
|
||||
}
|
||||
|
||||
51
yarn.lock
51
yarn.lock
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user