diff --git a/package.json b/package.json
index 710ec8c0..7f7d9c10 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/renderer/index.html b/src/renderer/index.html
index f0f81e5b..79267a30 100644
--- a/src/renderer/index.html
+++ b/src/renderer/index.html
@@ -7,7 +7,7 @@
+ 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:" />
diff --git a/src/renderer/src/components/app/Sidebar.tsx b/src/renderer/src/components/app/Sidebar.tsx
index 127d71fb..cb187c1c 100644
--- a/src/renderer/src/components/app/Sidebar.tsx
+++ b/src/renderer/src/components/app/Sidebar.tsx
@@ -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 (
-
+
diff --git a/src/renderer/src/hooks/useAppInitEffect.ts b/src/renderer/src/hooks/useAppInitEffect.ts
new file mode 100644
index 00000000..92495bc9
--- /dev/null
+++ b/src/renderer/src/hooks/useAppInitEffect.ts
@@ -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])
+}
diff --git a/src/renderer/src/hooks/useAvatar.ts b/src/renderer/src/hooks/useAvatar.ts
new file mode 100644
index 00000000..0c57d157
--- /dev/null
+++ b/src/renderer/src/hooks/useAvatar.ts
@@ -0,0 +1,5 @@
+import { useAppSelector } from '@renderer/store'
+
+export default function useAvatar() {
+ return useAppSelector((state) => state.runtime.avatar)
+}
diff --git a/src/renderer/src/pages/home/components/Message.tsx b/src/renderer/src/pages/home/components/Message.tsx
index 8f905db5..e4b000ba 100644
--- a/src/renderer/src/pages/home/components/Message.tsx
+++ b/src/renderer/src/pages/home/components/Message.tsx
@@ -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 (
-
- {message.role === 'assistant' ? : Y}
-
+ {message.role === 'assistant' ? : }
)
diff --git a/src/renderer/src/pages/settings/GeneralSettings.tsx b/src/renderer/src/pages/settings/GeneralSettings.tsx
index 7a2bcac9..454d475c 100644
--- a/src/renderer/src/pages/settings/GeneralSettings.tsx
+++ b/src/renderer/src/pages/settings/GeneralSettings.tsx
@@ -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 General Settings
+ const avatar = useAvatar()
+ const [messageApi, contextHolder] = message.useMessage()
+ const dispatch = useAppDispatch()
+
+ return (
+
+ {contextHolder}
+ General Settings
+
+
+ Avatar
+ {}}
+ 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
+ })
+ }
+ }}>
+
+
+
+
+
+ )
}
-const Container = styled.div`
- padding: 20px;
+const UserAvatar = styled(Avatar)`
+ cursor: pointer;
`
+
export default GeneralSettings
diff --git a/src/renderer/src/pages/settings/components/SettingComponent.tsx b/src/renderer/src/pages/settings/components/SettingComponent.tsx
index 5fcf6fe5..f58779e8 100644
--- a/src/renderer/src/pages/settings/components/SettingComponent.tsx
+++ b/src/renderer/src/pages/settings/components/SettingComponent.tsx
@@ -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);
+`
diff --git a/src/renderer/src/services/storage.ts b/src/renderer/src/services/storage.ts
index 40c3a5c3..8ab09b48 100644
--- a/src/renderer/src/services/storage.ts
+++ b/src/renderer/src/services/storage.ts
@@ -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:${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(IMAGE_PREFIX + name)
+ }
+
+ static async removeImage(name: string) {
+ await localforage.removeItem(IMAGE_PREFIX + name)
+ }
}
diff --git a/src/renderer/src/store/index.ts b/src/renderer/src/store/index.ts
index f75c5966..75017643 100644
--- a/src/renderer/src/store/index.ts
+++ b/src/renderer/src/store/index.ts
@@ -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
)
diff --git a/src/renderer/src/store/runtime.ts b/src/renderer/src/store/runtime.ts
new file mode 100644
index 00000000..999fed17
--- /dev/null
+++ b/src/renderer/src/store/runtime.ts
@@ -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) => {
+ state.avatar = action.payload || Logo
+ }
+ }
+})
+
+export const { setAvatar } = runtimeSlice.actions
+
+export default runtimeSlice.reducer
diff --git a/src/renderer/src/utils/index.ts b/src/renderer/src/utils/index.ts
index 1dd3bd77..0aebc309 100644
--- a/src/renderer/src/utils/index.ts
+++ b/src/renderer/src/utils/index.ts
@@ -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, interval = 200, stopTi
}
export const uuid = () => uuidv4()
+
+export const convertToBase64 = (file: File): Promise => {
+ 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
+ })
+}
diff --git a/yarn.lock b/yarn.lock
index bc6168be..b244071d 100644
--- a/yarn.lock
+++ b/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"