feat: add dark and light theme

This commit is contained in:
kangfenmao 2024-07-29 17:14:49 +08:00
parent b91081ef99
commit 50f08124d7
40 changed files with 611 additions and 159 deletions

View File

@ -26,6 +26,7 @@
"@electron-toolkit/utils": "^3.0.0", "@electron-toolkit/utils": "^3.0.0",
"@sentry/electron": "^5.2.0", "@sentry/electron": "^5.2.0",
"electron-log": "^5.1.5", "electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.1.7", "electron-updater": "^6.1.7",
"electron-window-state": "^5.0.3" "electron-window-state": "^5.0.3"
}, },

15
src/main/config.ts Normal file
View File

@ -0,0 +1,15 @@
import Store from 'electron-store'
export const appConfig = new Store()
export const titleBarOverlayDark = {
height: 41,
color: '#1f1f1f',
symbolColor: '#ffffff'
}
export const titleBarOverlayLight = {
height: 41,
color: '#f8f8f8',
symbolColor: '#000000'
}

View File

@ -5,8 +5,9 @@ import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import windowStateKeeper from 'electron-window-state' import windowStateKeeper from 'electron-window-state'
import { join } from 'path' import { join } from 'path'
import icon from '../../resources/icon.png?asset' import icon from '../../resources/icon.png?asset'
import AppUpdater from './updater' import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
import { saveFile } from './event' import { saveFile } from './event'
import AppUpdater from './updater'
function createWindow() { function createWindow() {
// Load the previous state with fallback to defaults // Load the previous state with fallback to defaults
@ -15,6 +16,8 @@ function createWindow() {
defaultHeight: 670 defaultHeight: 670
}) })
const theme = appConfig.get('theme') || 'light'
// Create the browser window. // Create the browser window.
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
x: mainWindowState.x, x: mainWindowState.x,
@ -26,11 +29,7 @@ function createWindow() {
show: true, show: true,
autoHideMenuBar: true, autoHideMenuBar: true,
titleBarStyle: 'hidden', titleBarStyle: 'hidden',
titleBarOverlay: { titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
height: 41,
color: '#1f1f1f',
symbolColor: '#eee'
},
trafficLightPosition: { x: 8, y: 12 }, trafficLightPosition: { x: 8, y: 12 },
...(process.platform === 'linux' ? { icon } : {}), ...(process.platform === 'linux' ? { icon } : {}),
webPreferences: { webPreferences: {
@ -118,6 +117,12 @@ app.whenReady().then(() => {
ipcMain.handle('save-file', saveFile) ipcMain.handle('save-file', saveFile)
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
appConfig.set('theme', theme)
mainWindow?.setTitleBarOverlay &&
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
})
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法) // 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.handle('check-for-update', async () => { ipcMain.handle('check-for-update', async () => {
autoUpdater.logger?.info('触发检查更新') autoUpdater.logger?.info('触发检查更新')

View File

@ -12,6 +12,7 @@ declare global {
openWebsite: (url: string) => void openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void setProxy: (proxy: string | undefined) => void
saveFile: (path: string, content: string) => void saveFile: (path: string, content: string) => void
setTheme: (theme: 'light' | 'dark') => void
} }
} }
} }

View File

@ -7,7 +7,8 @@ const api = {
checkForUpdate: () => ipcRenderer.invoke('check-for-update'), checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url), openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy), setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content) saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content),
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme)
} }
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to

View File

@ -4,13 +4,11 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<title>Cherry Studio</title> <title>Cherry Studio</title>
<meta name="viewport" content="initial-scale=1, width=device-width" /> <meta name="viewport" content="initial-scale=1, width=device-width" />
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; 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' data: *; img-src 'self' data:" />
</head> </head>
<body>
<body theme-mode="dark">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script type="module" src="/src/main.tsx"></script>
</body> </body>

View File

@ -1,20 +1,21 @@
import store, { persistor } from '@renderer/store' import store, { persistor } from '@renderer/store'
import { ConfigProvider } from 'antd'
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { HashRouter, Route, Routes } from 'react-router-dom' import { HashRouter, Route, Routes } from 'react-router-dom'
import { PersistGate } from 'redux-persist/integration/react' import { PersistGate } from 'redux-persist/integration/react'
import Sidebar from './components/app/Sidebar' import Sidebar from './components/app/Sidebar'
import TopViewContainer from './components/TopView' import TopViewContainer from './components/TopView'
import { AntdThemeConfig, getAntdLocale } from './config/antd'
import AppsPage from './pages/apps/AppsPage' import AppsPage from './pages/apps/AppsPage'
import HomePage from './pages/home/HomePage' import HomePage from './pages/home/HomePage'
import SettingsPage from './pages/settings/SettingsPage' import SettingsPage from './pages/settings/SettingsPage'
import TranslatePage from './pages/translate/TranslatePage' import TranslatePage from './pages/translate/TranslatePage'
import AntdProvider from './providers/AntdProvider'
import { ThemeProvider } from './providers/ThemeProvider'
function App(): JSX.Element { function App(): JSX.Element {
return ( return (
<ConfigProvider theme={AntdThemeConfig} locale={getAntdLocale()}>
<Provider store={store}> <Provider store={store}>
<ThemeProvider>
<AntdProvider>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>
<TopViewContainer> <TopViewContainer>
<HashRouter> <HashRouter>
@ -28,8 +29,9 @@ function App(): JSX.Element {
</HashRouter> </HashRouter>
</TopViewContainer> </TopViewContainer>
</PersistGate> </PersistGate>
</AntdProvider>
</ThemeProvider>
</Provider> </Provider>
</ConfigProvider>
) )
} }

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4563475 */ font-family: "iconfont"; /* Project id 4563475 */
src: url('iconfont.woff2?t=1722099305424') format('woff2'), src: url('iconfont.woff2?t=1722242729348') format('woff2'),
url('iconfont.woff?t=1722099305424') format('woff'), url('iconfont.woff?t=1722242729348') format('woff'),
url('iconfont.ttf?t=1722099305424') format('truetype'); url('iconfont.ttf?t=1722242729348') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,14 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-dark1:before {
content: "\e72f";
}
.icon-theme-light:before {
content: "\e6b7";
}
.icon-translate_line:before { .icon-translate_line:before {
content: "\e7de"; content: "\e7de";
} }

View File

@ -2,11 +2,6 @@
@import './markdown.scss'; @import './markdown.scss';
@import './scrollbar.scss'; @import './scrollbar.scss';
// @font-face {
// font-family: 'Playwrite';
// src: url(../fonts/Playwrite.ttf) format('truetype');
// }
:root { :root {
--color-white: #ffffff; --color-white: #ffffff;
--color-white-soft: #f8f8f8; --color-white-soft: #f8f8f8;
@ -28,17 +23,22 @@
--color-background-soft: var(--color-black-soft); --color-background-soft: var(--color-black-soft);
--color-background-mute: var(--color-black-mute); --color-background-mute: var(--color-black-mute);
--color-primary: #00b96b; --color-primary: #135200;
--color-primary-soft: #00b96b99; --color-primary-soft: #13520099;
--color-primary-mute: #00b96b33; --color-primary-mute: #13520033;
--color-text: var(--color-text-1); --color-text: var(--color-text-1);
--color-icon: #ffffff99; --color-icon: #ffffff99;
--color-icon-white: #ffffff; --color-icon-white: #ffffff;
--color-border: #ffffff20; --color-border: #ffffff20;
--color-error: #f44336; --color-error: #f44336;
--color-code-background: #323232;
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3);
--navbar-background: #1f1f1f; --navbar-background: #1f1f1f;
--sidebar-background: #1f1f1f;
--navbar-height: 42px; --navbar-height: 42px;
--sidebar-width: 55px; --sidebar-width: 55px;
--assistants-width: 245px; --assistants-width: 245px;
@ -48,6 +48,44 @@
--input-bar-height: 125px; --input-bar-height: 125px;
} }
body[theme-mode='light'] {
--color-white: #ffffff;
--color-white-soft: #f8f8f8;
--color-white-mute: #efefef;
--color-black: #1b1b1f;
--color-black-soft: #262626;
--color-black-mute: #363636;
--color-gray-1: #8e8e93;
--color-gray-2: #aeaeb2;
--color-gray-3: #c7c7cc;
--color-text-1: rgba(0, 0, 0, 1);
--color-text-2: rgba(0, 0, 0, 0.6);
--color-text-3: rgba(0, 0, 0, 0.38);
--color-background: #ffffff;
--color-background-soft: var(--color-white-soft);
--color-background-mute: var(--color-white-mute);
--color-primary: #00b96b;
--color-primary-soft: #00b96b99;
--color-primary-mute: #00b96b33;
--color-text: var(--color-text-1);
--color-icon: #00000099;
--color-icon-white: #000000;
--color-border: #00000028;
--color-error: #f44336;
--color-code-background: #e3e3e3;
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3);
--navbar-background: #f8f8f8;
--sidebar-background: #f8f8f8;
}
*, *,
*::before, *::before,
*::after { *::after {
@ -68,8 +106,18 @@ body {
line-height: 1.6; line-height: 1.6;
overflow: hidden; overflow: hidden;
background-size: cover; background-size: cover;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', font-family:
'Droid Sans', 'Helvetica Neue', sans-serif; -apple-system,
BlinkMacSystemFont,
'Microsoft YaHei',
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue' sans-serif;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
@ -97,3 +145,9 @@ body,
#inputbar .ant-input { #inputbar .ant-input {
resize: none; resize: none;
} }
.chat-nav-dropdown {
.ant-dropdown-menu {
padding-bottom: 12px;
}
}

View File

@ -1,5 +1,5 @@
.markdown { .markdown {
color: #f1f1f1; color: var(--color-text);
font-size: 15px; font-size: 15px;
line-height: 1.6; line-height: 1.6;
user-select: text; user-select: text;
@ -33,44 +33,36 @@
h1 { h1 {
font-size: 2em; font-size: 2em;
color: #fff;
} }
h2 { h2 {
font-size: 1.5em; font-size: 1.5em;
color: #fff;
} }
h3 { h3 {
font-size: 1.2em; font-size: 1.2em;
color: #fff;
} }
h4 { h4 {
font-size: 1em; font-size: 1em;
color: #fff;
} }
h5 { h5 {
font-size: 0.9em; font-size: 0.9em;
color: #fff;
} }
h6 { h6 {
font-size: 0.8em; font-size: 0.8em;
color: #fff;
} }
p { p {
margin: 1em 0; margin: 1em 0;
color: #fff;
} }
ul, ul,
ol { ol {
padding-left: 1.5em; padding-left: 1.5em;
margin: 1em 0; margin: 1em 0;
color: #ccc;
} }
li { li {

View File

@ -8,8 +8,8 @@
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15); background: var(--color-scrollbar-thumb);
&:hover { &:hover {
background: rgba(255, 255, 255, 0.3); background: var(--color-scrollbar-thumb-hover);
} }
} }

View File

@ -56,7 +56,7 @@ const Container = styled.div`
min-width: var(--sidebar-width); min-width: var(--sidebar-width);
min-height: 100%; min-height: 100%;
-webkit-app-region: drag !important; -webkit-app-region: drag !important;
background-color: #1f1f1f; background-color: var(--sidebar-background);
border-right: 0.5px solid var(--color-border); border-right: 0.5px solid var(--color-border);
padding-top: var(--navbar-height); padding-top: var(--navbar-height);
position: relative; position: relative;
@ -68,7 +68,7 @@ const AvatarImg = styled.img`
height: 28px; height: 28px;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
margin: 5px 0; margin: 5px 0;
margin-top: ${isMac ? '16px' : '7px'}; margin-top: ${isMac ? '16px' : '9px'};
` `
const MainMenus = styled.div` const MainMenus = styled.div`
display: flex; display: flex;
@ -102,7 +102,7 @@ const Icon = styled.div`
font-size: 17px; font-size: 17px;
} }
&:hover { &:hover {
background-color: #ffffff30; background-color: var(--color-background-soft);
cursor: pointer; cursor: pointer;
.iconfont, .iconfont,
.anticon { .anticon {
@ -110,7 +110,7 @@ const Icon = styled.div`
} }
} }
&.active { &.active {
background-color: #ffffff20; background-color: var(--color-background-mute);
.iconfont, .iconfont,
.anticon { .anticon {
color: var(--color-icon-white); color: var(--color-icon-white);

View File

@ -1,26 +0,0 @@
import store from '@renderer/store'
import { theme, ThemeConfig } from 'antd'
import zhCN from 'antd/locale/zh_CN'
export const colorPrimary = '#00b96b'
export const AntdThemeConfig: ThemeConfig = {
token: {
colorPrimary,
borderRadius: 5
},
algorithm: [theme.darkAlgorithm]
}
export function getAntdLocale() {
const language = store.getState().settings.language
switch (language) {
case 'zh-CN':
return zhCN
case 'en-US':
return undefined
default:
return zhCN
}
}

View File

@ -1,5 +1,10 @@
import { useAppDispatch, useAppSelector } from '@renderer/store' import { useAppDispatch, useAppSelector } from '@renderer/store'
import { setSendMessageShortcut as _setSendMessageShortcut, SendMessageShortcut } from '@renderer/store/settings' import {
setSendMessageShortcut as _setSendMessageShortcut,
SendMessageShortcut,
setTheme,
ThemeMode
} from '@renderer/store/settings'
export function useSettings() { export function useSettings() {
const settings = useAppSelector((state) => state.settings) const settings = useAppSelector((state) => state.settings)
@ -9,6 +14,9 @@ export function useSettings() {
...settings, ...settings,
setSendMessageShortcut(shortcut: SendMessageShortcut) { setSendMessageShortcut(shortcut: SendMessageShortcut) {
dispatch(_setSendMessageShortcut(shortcut)) dispatch(_setSendMessageShortcut(shortcut))
},
setTheme(theme: ThemeMode) {
dispatch(setTheme(theme))
} }
} }
} }

View File

@ -156,7 +156,11 @@ const resources = {
'about.feedback.button': 'Feedback', 'about.feedback.button': 'Feedback',
'about.contact.title': '📧 Contact', 'about.contact.title': '📧 Contact',
'about.contact.button': 'Email', 'about.contact.button': 'Email',
'proxy.title': 'Proxy Address' 'proxy.title': 'Proxy Address',
'theme.title': 'Theme',
'theme.dark': 'Dark',
'theme.light': 'Light',
'theme.auto': 'Auto'
}, },
translate: { translate: {
title: 'Translation', title: 'Translation',
@ -334,7 +338,11 @@ const resources = {
'about.feedback.button': '反馈', 'about.feedback.button': '反馈',
'about.contact.title': '📧 邮件联系', 'about.contact.title': '📧 邮件联系',
'about.contact.button': '邮件', 'about.contact.button': '邮件',
'proxy.title': '代理地址' 'proxy.title': '代理地址',
'theme.title': '主题',
'theme.dark': '深色主题',
'theme.light': '浅色主题',
'theme.auto': '跟随系统'
}, },
translate: { translate: {
title: '翻译', title: '翻译',

View File

@ -2,6 +2,7 @@ import localforage from 'localforage'
import KeyvStorage from '@kangfenmao/keyv-storage' import KeyvStorage from '@kangfenmao/keyv-storage'
import * as Sentry from '@sentry/electron/renderer' import * as Sentry from '@sentry/electron/renderer'
import { isProduction, loadScript } from './utils' import { isProduction, loadScript } from './utils'
import { ThemeMode } from './store/settings'
async function initSentry() { async function initSentry() {
if (await isProduction()) { if (await isProduction()) {
@ -21,12 +22,12 @@ async function initSentry() {
} }
} }
export async function initMermaid() { export async function initMermaid(theme: ThemeMode) {
if (!window.mermaid) { if (!window.mermaid) {
await loadScript('https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js') await loadScript('https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js')
window.mermaid.initialize({ window.mermaid.initialize({
startOnLoad: true, startOnLoad: true,
theme: 'dark', theme: theme === ThemeMode.dark ? 'dark' : 'default',
securityLevel: 'loose' securityLevel: 'loose'
}) })
window.mermaid.contentLoaded() window.mermaid.contentLoaded()

View File

@ -116,12 +116,16 @@ const AssistantCard = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-bottom: 16px; margin-bottom: 16px;
background-color: #111; background-color: var(--color-background-soft);
border: 0.5px solid #151515; border: 0.5px solid var(--color-border);
border-radius: 10px; border-radius: 10px;
padding: 15px; padding: 15px;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease-in-out;
&:hover {
background-color: var(--color-background-mute);
}
` `
const EmojiHeader = styled.div` const EmojiHeader = styled.div`
width: 25px; width: 25px;
@ -148,7 +152,7 @@ const AssistantName = styled(Title)`
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
color: #fff; color: var(--color-white);
font-weight: 900; font-weight: 900;
` `

View File

@ -9,6 +9,8 @@ import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore
import Navigation from './components/NavigationCenter' import Navigation from './components/NavigationCenter'
import { isMac, isWindows } from '@renderer/config/constant' import { isMac, isWindows } from '@renderer/config/constant'
import { Assistant } from '@renderer/types' import { Assistant } from '@renderer/types'
import { useTheme } from '@renderer/providers/ThemeProvider'
import { Switch } from 'antd'
let _activeAssistant: Assistant let _activeAssistant: Assistant
@ -18,6 +20,7 @@ const HomePage: FC = () => {
const { rightSidebarShown, toggleRightSidebar } = useShowRightSidebar() const { rightSidebarShown, toggleRightSidebar } = useShowRightSidebar()
const { showAssistants, toggleShowAssistants } = useShowAssistants() const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { defaultAssistant } = useDefaultAssistant() const { defaultAssistant } = useDefaultAssistant()
const { theme, toggleTheme } = useTheme()
_activeAssistant = activeAssistant _activeAssistant = activeAssistant
@ -42,6 +45,12 @@ const HomePage: FC = () => {
)} )}
<Navigation activeAssistant={activeAssistant} /> <Navigation activeAssistant={activeAssistant} />
<NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 8 }}> <NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 8 }}>
<ThemeSwitch
checkedChildren={<i className="iconfont icon-theme icon-dark1" />}
unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />}
defaultChecked={theme === 'dark'}
onChange={toggleTheme}
/>
<NewButton onClick={toggleRightSidebar}> <NewButton onClick={toggleRightSidebar}>
<i className={`iconfont ${rightSidebarShown ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} /> <i className={`iconfont ${rightSidebarShown ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
</NewButton> </NewButton>
@ -101,4 +110,12 @@ export const NewButton = styled.div`
} }
` `
const ThemeSwitch = styled(Switch)`
-webkit-app-region: none;
margin-right: 8px;
.icon-theme {
font-size: 14px;
}
`
export default HomePage export default HomePage

View File

@ -96,7 +96,9 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
<AssistantItem <AssistantItem
onClick={() => onSwitchAssistant(assistant)} onClick={() => onSwitchAssistant(assistant)}
className={assistant.id === activeAssistant?.id ? 'active' : ''}> className={assistant.id === activeAssistant?.id ? 'active' : ''}>
<AssistantName>{assistant.name || t('assistant.default.name')}</AssistantName> <AssistantName className="name">
{assistant.name || t('assistant.default.name')}
</AssistantName>
</AssistantItem> </AssistantItem>
</Dropdown> </Dropdown>
</div> </div>
@ -143,13 +145,15 @@ const AssistantItem = styled.div`
&.active { &.active {
background-color: var(--color-background-mute); background-color: var(--color-background-mute);
cursor: pointer; cursor: pointer;
.name {
font-weight: bolder;
}
} }
` `
const AssistantName = styled.div` const AssistantName = styled.div`
font-size: 14px; font-size: 14px;
color: var(--color-text-1); color: var(--color-text);
font-weight: 500;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;

View File

@ -1,11 +1,13 @@
import React from 'react' import { CheckOutlined, CopyOutlined } from '@ant-design/icons'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
import styled from 'styled-components'
import { CopyOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import Mermaid from './Mermaid'
import { initMermaid } from '@renderer/init' import { initMermaid } from '@renderer/init'
import { useTheme } from '@renderer/providers/ThemeProvider'
import { ThemeMode } from '@renderer/store/settings'
import React, { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { atomDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
import styled from 'styled-components'
import Mermaid from './Mermaid'
interface CodeBlockProps { interface CodeBlockProps {
children: string children: string
@ -15,16 +17,20 @@ interface CodeBlockProps {
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) => { const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) => {
const match = /language-(\w+)/.exec(className || '') const match = /language-(\w+)/.exec(className || '')
const [copied, setCopied] = useState(false)
const { theme } = useTheme()
const { t } = useTranslation() const { t } = useTranslation()
const onCopy = () => { const onCopy = () => {
navigator.clipboard.writeText(children) navigator.clipboard.writeText(children)
window.message.success({ content: t('message.copied'), key: 'copy-code' }) window.message.success({ content: t('message.copied'), key: 'copy-code' })
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} }
if (match && match[1] === 'mermaid') { if (match && match[1] === 'mermaid') {
initMermaid() initMermaid(theme)
return <Mermaid chart={children} /> return <Mermaid chart={children} />
} }
@ -32,12 +38,13 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) =
<div> <div>
<CodeHeader> <CodeHeader>
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage> <CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
<CopyOutlined className="copy" onClick={onCopy} /> {!copied && <CopyOutlined className="copy" onClick={onCopy} />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</CodeHeader> </CodeHeader>
<SyntaxHighlighter <SyntaxHighlighter
{...rest} {...rest}
language={match[1]} language={match[1]}
style={atomDark} style={theme === ThemeMode.dark ? atomDark : oneLight}
wrapLongLines={true} wrapLongLines={true}
customStyle={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, marginTop: 0 }}> customStyle={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, marginTop: 0 }}>
{String(children).replace(/\n$/, '')} {String(children).replace(/\n$/, '')}
@ -54,10 +61,10 @@ const CodeHeader = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
color: #fff; color: var(--color-text);
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
background-color: #323232; background-color: var(--color-code-background);
height: 40px; height: 40px;
padding: 0 10px; padding: 0 10px;
border-top-left-radius: 8px; border-top-left-radius: 8px;

View File

@ -248,7 +248,7 @@ const ToolbarButton = styled(Button)`
&:hover { &:hover {
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
.anticon { .anticon {
color: white; color: var(--color-text-1);
} }
} }
` `
@ -260,7 +260,7 @@ const TextCount = styled.div`
font-size: 11px; font-size: 11px;
color: var(--color-text-3); color: var(--color-text-3);
z-index: 10; z-index: 10;
background-color: #121212; background-color: var(--color-background-soft);
padding: 2px 8px; padding: 2px 8px;
border-top-left-radius: 7px; border-top-left-radius: 7px;
user-select: none; user-select: none;

View File

@ -1,16 +1,23 @@
import { CopyOutlined, DeleteOutlined, EditOutlined, MenuOutlined, SaveOutlined, SyncOutlined } from '@ant-design/icons' import {
import Logo from '@renderer/assets/images/logo.png' CheckOutlined,
CopyOutlined,
DeleteOutlined,
EditOutlined,
MenuOutlined,
SaveOutlined,
SyncOutlined
} from '@ant-design/icons'
import { getModelLogo } from '@renderer/config/provider' import { getModelLogo } from '@renderer/config/provider'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event' import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message } from '@renderer/types' import { Message } from '@renderer/types'
import { firstLetter } from '@renderer/utils' import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
import { Avatar, Dropdown, Tooltip } from 'antd' import { Avatar, Dropdown, Tooltip } from 'antd'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { isEmpty, upperFirst } from 'lodash' import { isEmpty, upperFirst } from 'lodash'
import { FC, useCallback } from 'react' import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Markdown from 'react-markdown' import Markdown from 'react-markdown'
import styled from 'styled-components' import styled from 'styled-components'
@ -31,6 +38,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const { assistant } = useAssistant(message.assistantId) const { assistant } = useAssistant(message.assistantId)
const { userName, showMessageDivider, messageFont } = useSettings() const { userName, showMessageDivider, messageFont } = useSettings()
const { generating } = useRuntime() const { generating } = useRuntime()
const [copied, setCopied] = useState(false)
const isLastMessage = index === 0 const isLastMessage = index === 0
const isUserMessage = message.role === 'user' const isUserMessage = message.role === 'user'
@ -39,6 +47,8 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const onCopy = () => { const onCopy = () => {
navigator.clipboard.writeText(message.content) navigator.clipboard.writeText(message.content)
window.message.success({ content: t('message.copied'), key: 'copy-message' }) window.message.success({ content: t('message.copied'), key: 'copy-message' })
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} }
const onDelete = async () => { const onDelete = async () => {
@ -105,14 +115,14 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
<MessageHeader> <MessageHeader>
<AvatarWrapper> <AvatarWrapper>
{message.role === 'assistant' ? ( {message.role === 'assistant' ? (
<Avatar src={message.modelId ? getModelLogo(message.modelId) : Logo} size={35}> <Avatar src={message.modelId ? getModelLogo(message.modelId) : undefined} size={35}>
{firstLetter(message.modelId).toUpperCase()} {firstLetter(assistant?.name).toUpperCase()}
</Avatar> </Avatar>
) : ( ) : (
<Avatar src={avatar} size={35} /> <Avatar src={avatar} size={35} />
)} )}
<UserWrap> <UserWrap>
<UserName>{getUserName()}</UserName> <UserName>{removeLeadingEmoji(getUserName())}</UserName>
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime> <MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</UserWrap> </UserWrap>
</AvatarWrapper> </AvatarWrapper>
@ -137,25 +147,26 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}> <MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
{message.role === 'user' && ( {message.role === 'user' && (
<Tooltip title="Edit" mouseEnterDelay={0.8}> <Tooltip title="Edit" mouseEnterDelay={0.8}>
<ActionButton> <ActionButton onClick={onEdit}>
<EditOutlined onClick={onEdit} /> <EditOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
)} )}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}> <Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<ActionButton> <ActionButton onClick={onCopy}>
<CopyOutlined onClick={onCopy} /> {!copied && <CopyOutlined />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}> <Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
<ActionButton> <ActionButton onClick={onDelete}>
<DeleteOutlined onClick={onDelete} /> <DeleteOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
{canRegenerate && ( {canRegenerate && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}> <Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton> <ActionButton onClick={onRegenerate}>
<SyncOutlined onClick={onRegenerate} /> <SyncOutlined />
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
)} )}

View File

@ -1,6 +1,5 @@
import { CodeSandboxOutlined } from '@ant-design/icons' import { CodeSandboxOutlined } from '@ant-design/icons'
import { NavbarCenter } from '@renderer/components/app/Navbar' import { NavbarCenter } from '@renderer/components/app/Navbar'
import { colorPrimary } from '@renderer/config/antd'
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant' import { useAssistant } from '@renderer/hooks/useAssistant'
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
@ -13,6 +12,7 @@ import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { NewButton } from '../HomePage' import { NewButton } from '../HomePage'
import { getModelLogo } from '@renderer/config/provider' import { getModelLogo } from '@renderer/config/provider'
import { removeLeadingEmoji } from '@renderer/utils'
interface Props { interface Props {
activeAssistant: Assistant activeAssistant: Assistant
@ -34,7 +34,7 @@ const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
children: p.models.map((m) => ({ children: p.models.map((m) => ({
key: m.id, key: m.id,
label: upperFirst(m.name), label: upperFirst(m.name),
style: m.id === model?.id ? { color: colorPrimary } : undefined, style: m.id === model?.id ? { color: 'var(--color-primary)' } : undefined,
icon: <Avatar src={getModelLogo(m.id)} size={24} />, icon: <Avatar src={getModelLogo(m.id)} size={24} />,
onClick: () => setModel(m) onClick: () => setModel(m)
})) }))
@ -47,8 +47,11 @@ const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
<i className="iconfont icon-showsidebarhoriz" /> <i className="iconfont icon-showsidebarhoriz" />
</NewButton> </NewButton>
)} )}
<AssistantName>{assistant?.name || t('assistant.default.name')}</AssistantName> <AssistantName>{removeLeadingEmoji(assistant?.name) || t('assistant.default.name')}</AssistantName>
<DropdownMenu menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']}> <DropdownMenu
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }}
trigger={['click']}
overlayClassName="chat-nav-dropdown">
<DropdownButton size="small" type="primary" ghost> <DropdownButton size="small" type="primary" ghost>
<CodeSandboxOutlined /> <CodeSandboxOutlined />
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName> <ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
@ -69,13 +72,14 @@ const AssistantName = styled.span`
` `
const DropdownButton = styled(Button)` const DropdownButton = styled(Button)`
font-size: 10px; font-size: 11px;
border-radius: 15px; border-radius: 15px;
padding: 0 8px; padding: 0 8px;
` `
const ModelName = styled.span` const ModelName = styled.span`
margin-left: -2px; margin-left: -2px;
font-weight: bolder;
` `
export default NavigationCenter export default NavigationCenter

View File

@ -90,10 +90,10 @@ const Tab = styled.div`
align-items: center; align-items: center;
font-size: 13px; font-size: 13px;
cursor: pointer; cursor: pointer;
color: #8a8a8a; color: var(--color-text-3);
border-bottom: 1px solid transparent; border-bottom: 1px solid transparent;
&.active { &.active {
color: #bbb; color: var(--color-text-2);
font-weight: 600; font-weight: 600;
} }
` `

View File

@ -130,7 +130,7 @@ const TopicListItem = styled.div`
margin-bottom: 5px; margin-bottom: 5px;
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 5px;
font-size: 13px; font-size: 14px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -138,7 +138,8 @@ const TopicListItem = styled.div`
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
} }
&.active { &.active {
background-color: var(--color-background-soft); background-color: var(--color-background-mute);
font-weight: bolder;
} }
` `

View File

@ -8,14 +8,14 @@ import useAvatar from '@renderer/hooks/useAvatar'
import { useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime' import { setAvatar } from '@renderer/store/runtime'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { setLanguage, setUserName } from '@renderer/store/settings' import { setLanguage, setUserName, ThemeMode } from '@renderer/store/settings'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings' import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
const GeneralSettings: FC = () => { const GeneralSettings: FC = () => {
const avatar = useAvatar() const avatar = useAvatar()
const { language, proxyUrl: storeProxyUrl, userName } = useSettings() const { language, proxyUrl: storeProxyUrl, userName, theme, setTheme } = useSettings()
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl) const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { t } = useTranslation() const { t } = useTranslation()
@ -53,6 +53,20 @@ const GeneralSettings: FC = () => {
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
<Select
defaultValue={theme}
style={{ width: 120 }}
onChange={setTheme}
options={[
{ value: ThemeMode.light, label: t('settings.theme.light') },
{ value: ThemeMode.dark, label: t('settings.theme.dark') },
{ value: ThemeMode.auto, label: t('settings.theme.auto') }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('common.avatar')}</SettingRowTitle> <SettingRowTitle>{t('common.avatar')}</SettingRowTitle>
<Upload <Upload

View File

@ -100,11 +100,11 @@ const ProviderSettings: FC = () => {
key={JSON.stringify(provider)} key={JSON.stringify(provider)}
className={provider.id === selectedProvider?.id ? 'active' : ''} className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}> onClick={() => setSelectedProvider(provider)}>
{provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={28} />} {provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={25} />}
{!provider.isSystem && ( {!provider.isSystem && (
<Avatar <Avatar
size={28} size={25}
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 28 }}> style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 25 }}>
{getFirstCharacter(provider.name)} {getFirstCharacter(provider.name)}
</Avatar> </Avatar>
)} )}
@ -151,7 +151,7 @@ const ProviderListContainer = styled.div`
width: var(--assistants-width); width: var(--assistants-width);
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border); border-right: 0.5px solid var(--color-border);
padding: 10px 8px; padding: 8px;
overflow-y: auto; overflow-y: auto;
` `
@ -173,20 +173,21 @@ const ProviderListItem = styled.div`
font-size: 14px; font-size: 14px;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
&:hover { &:hover {
background: #135200; background: var(--color-primary-mute);
} }
&.active { &.active {
background: #135200; background: var(--color-primary);
font-weight: bold; color: var(--color-white);
font-weight: bold !important;
} }
` `
const ProviderItemName = styled.div` const ProviderItemName = styled.div`
margin-left: 10px; margin-left: 10px;
font-weight: bold;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
font-weight: 500;
` `
const AddButtonWrapper = styled.div` const AddButtonWrapper = styled.div`

View File

@ -68,7 +68,7 @@ const SettingMenus = styled.ul`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: var(--assistants-width); min-width: var(--assistants-width);
border-right: 1px solid var(--color-border); border-right: 0.5px solid var(--color-border);
padding: 10px; padding: 10px;
` `
@ -84,13 +84,14 @@ const MenuItem = styled.li`
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 5px;
font-size: 14px; font-size: 14px;
font-weight: 500;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
&:hover { &:hover {
background: #135200; background: var(--color-primary-soft);
} }
&.active { &.active {
background: #135200; background: var(--color-primary);
font-weight: bold; color: var(--color-white);
} }
` `

View File

@ -178,7 +178,8 @@ const ListHeader = styled.div`
justify-content: space-between; justify-content: space-between;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
padding: 8px 22px; padding: 8px 22px;
color: #ffffff50; color: var(--color-white);
opacity: 0.4;
` `
const ListItem = styled.div` const ListItem = styled.div`
@ -199,14 +200,14 @@ const ListItemHeader = styled.div`
` `
const ListItemName = styled.div` const ListItemName = styled.div`
color: #fff; color: var(--color-white);
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
margin-left: 6px; margin-left: 6px;
` `
const ModelHeaderTitle = styled.div` const ModelHeaderTitle = styled.div`
color: #fff; color: var(--color-white);
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
margin-right: 10px; margin-right: 10px;

View File

@ -20,6 +20,7 @@ import Link from 'antd/es/typography/Link'
import { checkApi } from '@renderer/services/api' import { checkApi } from '@renderer/services/api'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { PROVIDER_CONFIG } from '@renderer/config/provider' import { PROVIDER_CONFIG } from '@renderer/config/provider'
import { useTheme } from '@renderer/providers/ThemeProvider'
interface Props { interface Props {
provider: Provider provider: Provider
@ -33,6 +34,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
const [apiChecking, setApiChecking] = useState(false) const [apiChecking, setApiChecking] = useState(false)
const { updateProvider, models, removeModel } = useProvider(provider.id) const { updateProvider, models, removeModel } = useProvider(provider.id)
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme()
const modelGroups = groupBy(models, 'group') const modelGroups = groupBy(models, 'group')
@ -68,13 +70,18 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
} }
return ( return (
<SettingContainer> <SettingContainer
style={
theme === 'dark'
? { backgroundColor: 'var(--color-background)' }
: { backgroundColor: 'var(--color-background-mute)' }
}>
<SettingTitle> <SettingTitle>
<Flex align="center"> <Flex align="center">
<span>{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}</span> <span>{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}</span>
{officialWebsite! && ( {officialWebsite! && (
<Link target="_blank" href={providerConfig.websites.official}> <Link target="_blank" href={providerConfig.websites.official}>
<ExportOutlined style={{ marginLeft: '8px', color: 'white', fontSize: '12px' }} /> <ExportOutlined style={{ marginLeft: '8px', color: 'var(--color-text)', fontSize: '12px' }} />
</Link> </Link>
)} )}
</Flex> </Flex>
@ -183,7 +190,8 @@ const HelpTextRow = styled.div`
const HelpText = styled.div` const HelpText = styled.div`
font-size: 11px; font-size: 11px;
color: #ffffff50; color: var(--color-text);
opacity: 0.4;
` `
const HelpLink = styled(Link)` const HelpLink = styled(Link)`

View File

@ -0,0 +1,37 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { ConfigProvider, theme } from 'antd'
import zhCN from 'antd/locale/zh_CN'
import { FC, PropsWithChildren } from 'react'
import { useTheme } from './ThemeProvider'
const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
const { language } = useSettings()
const { theme: _theme } = useTheme()
return (
<ConfigProvider
locale={getAntdLocale(language)}
theme={{
algorithm: [_theme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm],
token: {
colorPrimary: '#00b96b',
borderRadius: 5
}
}}>
{children}
</ConfigProvider>
)
}
function getAntdLocale(language: string) {
switch (language) {
case 'zh-CN':
return zhCN
case 'en-US':
return undefined
default:
return zhCN
}
}
export default AntdProvider

View File

@ -0,0 +1,43 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { ThemeMode } from '@renderer/store/settings'
import React, { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
interface ThemeContextType {
theme: ThemeMode
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType>({
theme: ThemeMode.light,
toggleTheme: () => {}
})
export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
const { theme, setTheme } = useSettings()
const [_theme, _setTheme] = useState(theme)
const toggleTheme = () => {
setTheme(theme === ThemeMode.dark ? ThemeMode.light : ThemeMode.dark)
}
useEffect((): any => {
if (theme === ThemeMode.auto) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
_setTheme(mediaQuery.matches ? ThemeMode.dark : ThemeMode.light)
const handleChange = (e: MediaQueryListEvent) => _setTheme(e.matches ? ThemeMode.dark : ThemeMode.light)
mediaQuery.addEventListener('change', handleChange)
return () => mediaQuery.removeEventListener('change', handleChange)
} else {
_setTheme(theme)
}
}, [theme])
useEffect(() => {
document.body.setAttribute('theme-mode', _theme)
window.api.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light')
}, [_theme])
return <ThemeContext.Provider value={{ theme: _theme, toggleTheme }}>{children}</ThemeContext.Provider>
}
export const useTheme = () => useContext(ThemeContext)

View File

@ -19,7 +19,7 @@ const persistedReducer = persistReducer(
{ {
key: 'cherry-studio', key: 'cherry-studio',
storage, storage,
version: 16, version: 17,
blacklist: ['runtime'], blacklist: ['runtime'],
migrate migrate
}, },

View File

@ -261,6 +261,15 @@ const migrateConfig = {
showInputEstimatedTokens: false showInputEstimatedTokens: false
} }
} }
},
'17': (state: RootState) => {
return {
...state,
settings: {
...state.settings,
theme: 'auto'
}
}
} }
} }

View File

@ -2,6 +2,12 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
export type SendMessageShortcut = 'Enter' | 'Shift+Enter' export type SendMessageShortcut = 'Enter' | 'Shift+Enter'
export enum ThemeMode {
light = 'light',
dark = 'dark',
auto = 'auto'
}
export interface SettingsState { export interface SettingsState {
showRightSidebar: boolean showRightSidebar: boolean
showAssistants: boolean showAssistants: boolean
@ -12,6 +18,7 @@ export interface SettingsState {
showMessageDivider: boolean showMessageDivider: boolean
messageFont: 'system' | 'serif' messageFont: 'system' | 'serif'
showInputEstimatedTokens: boolean showInputEstimatedTokens: boolean
theme: ThemeMode
} }
const initialState: SettingsState = { const initialState: SettingsState = {
@ -23,7 +30,8 @@ const initialState: SettingsState = {
userName: '', userName: '',
showMessageDivider: true, showMessageDivider: true,
messageFont: 'system', messageFont: 'system',
showInputEstimatedTokens: false showInputEstimatedTokens: false,
theme: ThemeMode.light
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({
@ -59,6 +67,9 @@ const settingsSlice = createSlice({
}, },
setShowInputEstimatedTokens: (state, action: PayloadAction<boolean>) => { setShowInputEstimatedTokens: (state, action: PayloadAction<boolean>) => {
state.showInputEstimatedTokens = action.payload state.showInputEstimatedTokens = action.payload
},
setTheme: (state, action: PayloadAction<ThemeMode>) => {
state.theme = action.payload
} }
} }
}) })
@ -73,7 +84,8 @@ export const {
setUserName, setUserName,
setShowMessageDivider, setShowMessageDivider,
setMessageFont, setMessageFont,
setShowInputEstimatedTokens setShowInputEstimatedTokens,
setTheme
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer

View File

@ -93,9 +93,14 @@ export function droppableReorder<T>(list: T[], startIndex: number, endIndex: num
return result return result
} }
// firstLetter export function firstLetter(str: string): string {
export const firstLetter = (str?: string) => { const match = str.match(/\p{L}\p{M}*|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/u)
return str ? str[0] : '' return match ? match[0] : ''
}
export function removeLeadingEmoji(str: string): string {
const emojiRegex = /^(\p{Emoji_Presentation}|\p{Emoji}\uFE0F)+/u
return str.replace(emojiRegex, '')
} }
export function isFreeModel(model: Model) { export function isFreeModel(model: Model) {

207
yarn.lock
View File

@ -2710,6 +2710,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ajv-formats@npm:^2.1.1":
version: 2.1.1
resolution: "ajv-formats@npm:2.1.1"
dependencies:
ajv: "npm:^8.0.0"
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
checksum: 10c0/e43ba22e91b6a48d96224b83d260d3a3a561b42d391f8d3c6d2c1559f9aa5b253bfb306bc94bbeca1d967c014e15a6efe9a207309e95b3eaae07fcbcdc2af662
languageName: node
linkType: hard
"ajv-keywords@npm:^3.4.1": "ajv-keywords@npm:^3.4.1":
version: 3.5.2 version: 3.5.2
resolution: "ajv-keywords@npm:3.5.2" resolution: "ajv-keywords@npm:3.5.2"
@ -2731,6 +2745,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ajv@npm:^8.0.0, ajv@npm:^8.6.3":
version: 8.17.1
resolution: "ajv@npm:8.17.1"
dependencies:
fast-deep-equal: "npm:^3.1.3"
fast-uri: "npm:^3.0.1"
json-schema-traverse: "npm:^1.0.0"
require-from-string: "npm:^2.0.2"
checksum: 10c0/ec3ba10a573c6b60f94639ffc53526275917a2df6810e4ab5a6b959d87459f9ef3f00d5e7865b82677cb7d21590355b34da14d1d0b9c32d75f95a187e76fff35
languageName: node
linkType: hard
"ansi-regex@npm:^5.0.1": "ansi-regex@npm:^5.0.1":
version: 5.0.1 version: 5.0.1
resolution: "ansi-regex@npm:5.0.1" resolution: "ansi-regex@npm:5.0.1"
@ -3050,6 +3076,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"atomically@npm:^1.7.0":
version: 1.7.0
resolution: "atomically@npm:1.7.0"
checksum: 10c0/31f5efd5d69474681268557af4024f9e10223bb6b39fdedb5f2e19405186c4b76284fac9f6c43c9af75013cad6437e93b7168268f5ddb7aaf1cfc5fdb415f227
languageName: node
linkType: hard
"available-typed-arrays@npm:^1.0.7": "available-typed-arrays@npm:^1.0.7":
version: 1.0.7 version: 1.0.7
resolution: "available-typed-arrays@npm:1.0.7" resolution: "available-typed-arrays@npm:1.0.7"
@ -3424,6 +3457,7 @@ __metadata:
electron-builder: "npm:^24.9.1" electron-builder: "npm:^24.9.1"
electron-devtools-installer: "npm:^3.2.0" electron-devtools-installer: "npm:^3.2.0"
electron-log: "npm:^5.1.5" electron-log: "npm:^5.1.5"
electron-store: "npm:^8.2.0"
electron-updater: "npm:^6.1.7" electron-updater: "npm:^6.1.7"
electron-vite: "npm:^2.0.0" electron-vite: "npm:^2.0.0"
electron-window-state: "npm:^5.0.3" electron-window-state: "npm:^5.0.3"
@ -3632,6 +3666,24 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"conf@npm:^10.2.0":
version: 10.2.0
resolution: "conf@npm:10.2.0"
dependencies:
ajv: "npm:^8.6.3"
ajv-formats: "npm:^2.1.1"
atomically: "npm:^1.7.0"
debounce-fn: "npm:^4.0.0"
dot-prop: "npm:^6.0.1"
env-paths: "npm:^2.2.1"
json-schema-typed: "npm:^7.0.3"
onetime: "npm:^5.1.2"
pkg-up: "npm:^3.1.0"
semver: "npm:^7.3.5"
checksum: 10c0/d608d8c54ba7fad368eac640e77f2ce0334ec27cfd62ac39f44e361af8af9915eaa6c2ada81fbc25c3219273d972b4868bc752e8e2116cb6e12d35df72dc25a4
languageName: node
linkType: hard
"config-file-ts@npm:^0.2.4": "config-file-ts@npm:^0.2.4":
version: 0.2.6 version: 0.2.6
resolution: "config-file-ts@npm:0.2.6" resolution: "config-file-ts@npm:0.2.6"
@ -3766,6 +3818,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"debounce-fn@npm:^4.0.0":
version: 4.0.0
resolution: "debounce-fn@npm:4.0.0"
dependencies:
mimic-fn: "npm:^3.0.0"
checksum: 10c0/bcbd8eb253bdb6ee2f32759c95973c62bc479e74efbe1a44e17acfb0ea7d4bcbe615bf7e34aab80247ac08669c1ab72f7da0f384ceb7f15c18333d31d9030384
languageName: node
linkType: hard
"debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4": "debug@npm:4, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4":
version: 4.3.4 version: 4.3.4
resolution: "debug@npm:4.3.4" resolution: "debug@npm:4.3.4"
@ -3961,6 +4022,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dot-prop@npm:^6.0.1":
version: 6.0.1
resolution: "dot-prop@npm:6.0.1"
dependencies:
is-obj: "npm:^2.0.0"
checksum: 10c0/30e51ec6408978a6951b21e7bc4938aad01a86f2fdf779efe52330205c6bb8a8ea12f35925c2029d6dc9d1df22f916f32f828ce1e9b259b1371c580541c22b5a
languageName: node
linkType: hard
"dotenv-cli@npm:^7.4.2": "dotenv-cli@npm:^7.4.2":
version: 7.4.2 version: 7.4.2
resolution: "dotenv-cli@npm:7.4.2" resolution: "dotenv-cli@npm:7.4.2"
@ -4077,6 +4147,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"electron-store@npm:^8.2.0":
version: 8.2.0
resolution: "electron-store@npm:8.2.0"
dependencies:
conf: "npm:^10.2.0"
type-fest: "npm:^2.17.0"
checksum: 10c0/a4d19827e96ab67bf6c2a375910f51b147b23f4a0468da5cfeeb069acdfdbcd3a9f5650248a62a05aa0967149e4d1c47f2d0ba7582205e5eb38952c93b6882e1
languageName: node
linkType: hard
"electron-to-chromium@npm:^1.4.668": "electron-to-chromium@npm:^1.4.668":
version: 1.4.776 version: 1.4.776
resolution: "electron-to-chromium@npm:1.4.776" resolution: "electron-to-chromium@npm:1.4.776"
@ -4184,7 +4264,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"env-paths@npm:^2.2.0": "env-paths@npm:^2.2.0, env-paths@npm:^2.2.1":
version: 2.2.1 version: 2.2.1
resolution: "env-paths@npm:2.2.1" resolution: "env-paths@npm:2.2.1"
checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4
@ -4809,6 +4889,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fast-uri@npm:^3.0.1":
version: 3.0.1
resolution: "fast-uri@npm:3.0.1"
checksum: 10c0/3cd46d6006083b14ca61ffe9a05b8eef75ef87e9574b6f68f2e17ecf4daa7aaadeff44e3f0f7a0ef4e0f7e7c20fc07beec49ff14dc72d0b500f00386592f2d10
languageName: node
linkType: hard
"fastq@npm:^1.6.0": "fastq@npm:^1.6.0":
version: 1.17.1 version: 1.17.1
resolution: "fastq@npm:1.17.1" resolution: "fastq@npm:1.17.1"
@ -4863,6 +4950,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"find-up@npm:^3.0.0":
version: 3.0.0
resolution: "find-up@npm:3.0.0"
dependencies:
locate-path: "npm:^3.0.0"
checksum: 10c0/2c2e7d0a26db858e2f624f39038c74739e38306dee42b45f404f770db357947be9d0d587f1cac72d20c114deb38aa57316e879eb0a78b17b46da7dab0a3bd6e3
languageName: node
linkType: hard
"find-up@npm:^5.0.0": "find-up@npm:^5.0.0":
version: 5.0.0 version: 5.0.0
resolution: "find-up@npm:5.0.0" resolution: "find-up@npm:5.0.0"
@ -5883,6 +5979,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-obj@npm:^2.0.0":
version: 2.0.0
resolution: "is-obj@npm:2.0.0"
checksum: 10c0/85044ed7ba8bd169e2c2af3a178cacb92a97aa75de9569d02efef7f443a824b5e153eba72b9ae3aca6f8ce81955271aa2dc7da67a8b720575d3e38104208cb4e
languageName: node
linkType: hard
"is-path-inside@npm:^3.0.3": "is-path-inside@npm:^3.0.3":
version: 3.0.3 version: 3.0.3
resolution: "is-path-inside@npm:3.0.3" resolution: "is-path-inside@npm:3.0.3"
@ -6135,6 +6238,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"json-schema-traverse@npm:^1.0.0":
version: 1.0.0
resolution: "json-schema-traverse@npm:1.0.0"
checksum: 10c0/71e30015d7f3d6dc1c316d6298047c8ef98a06d31ad064919976583eb61e1018a60a0067338f0f79cabc00d84af3fcc489bd48ce8a46ea165d9541ba17fb30c6
languageName: node
linkType: hard
"json-schema-typed@npm:^7.0.3":
version: 7.0.3
resolution: "json-schema-typed@npm:7.0.3"
checksum: 10c0/b4a6d984dd91f9aba72df8768c5ced99e789b8e17b55ee24afb3a687ce55b70a7b3f4360cac67939e1ff98e136ca26f3aa530635c13ef371ae5edc48b69a65f6
languageName: node
linkType: hard
"json-stable-stringify-without-jsonify@npm:^1.0.1": "json-stable-stringify-without-jsonify@npm:^1.0.1":
version: 1.0.1 version: 1.0.1
resolution: "json-stable-stringify-without-jsonify@npm:1.0.1" resolution: "json-stable-stringify-without-jsonify@npm:1.0.1"
@ -6269,6 +6386,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"locate-path@npm:^3.0.0":
version: 3.0.0
resolution: "locate-path@npm:3.0.0"
dependencies:
p-locate: "npm:^3.0.0"
path-exists: "npm:^3.0.0"
checksum: 10c0/3db394b7829a7fe2f4fbdd25d3c4689b85f003c318c5da4052c7e56eed697da8f1bce5294f685c69ff76e32cba7a33629d94396976f6d05fb7f4c755c5e2ae8b
languageName: node
linkType: hard
"locate-path@npm:^6.0.0": "locate-path@npm:^6.0.0":
version: 6.0.0 version: 6.0.0
resolution: "locate-path@npm:6.0.0" resolution: "locate-path@npm:6.0.0"
@ -6817,6 +6944,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mimic-fn@npm:^2.1.0":
version: 2.1.0
resolution: "mimic-fn@npm:2.1.0"
checksum: 10c0/b26f5479d7ec6cc2bce275a08f146cf78f5e7b661b18114e2506dd91ec7ec47e7a25bf4360e5438094db0560bcc868079fb3b1fb3892b833c1ecbf63f80c95a4
languageName: node
linkType: hard
"mimic-fn@npm:^3.0.0":
version: 3.1.0
resolution: "mimic-fn@npm:3.1.0"
checksum: 10c0/a07cdd8ed6490c2dff5b11f889b245d9556b80f5a653a552a651d17cff5a2d156e632d235106c2369f00cccef4071704589574cf3601bc1b1400a1f620dff067
languageName: node
linkType: hard
"mimic-response@npm:^1.0.0": "mimic-response@npm:^1.0.0":
version: 1.0.1 version: 1.0.1
resolution: "mimic-response@npm:1.0.1" resolution: "mimic-response@npm:1.0.1"
@ -7214,6 +7355,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"onetime@npm:^5.1.2":
version: 5.1.2
resolution: "onetime@npm:5.1.2"
dependencies:
mimic-fn: "npm:^2.1.0"
checksum: 10c0/ffcef6fbb2692c3c40749f31ea2e22677a876daea92959b8a80b521d95cca7a668c884d8b2045d1d8ee7d56796aa405c405462af112a1477594cc63531baeb8f
languageName: node
linkType: hard
"openai-chat-tokens@npm:^0.2.8": "openai-chat-tokens@npm:^0.2.8":
version: 0.2.8 version: 0.2.8
resolution: "openai-chat-tokens@npm:0.2.8" resolution: "openai-chat-tokens@npm:0.2.8"
@ -7273,6 +7423,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"p-limit@npm:^2.0.0":
version: 2.3.0
resolution: "p-limit@npm:2.3.0"
dependencies:
p-try: "npm:^2.0.0"
checksum: 10c0/8da01ac53efe6a627080fafc127c873da40c18d87b3f5d5492d465bb85ec7207e153948df6b9cbaeb130be70152f874229b8242ee2be84c0794082510af97f12
languageName: node
linkType: hard
"p-limit@npm:^3.0.2": "p-limit@npm:^3.0.2":
version: 3.1.0 version: 3.1.0
resolution: "p-limit@npm:3.1.0" resolution: "p-limit@npm:3.1.0"
@ -7282,6 +7441,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"p-locate@npm:^3.0.0":
version: 3.0.0
resolution: "p-locate@npm:3.0.0"
dependencies:
p-limit: "npm:^2.0.0"
checksum: 10c0/7b7f06f718f19e989ce6280ed4396fb3c34dabdee0df948376483032f9d5ec22fdf7077ec942143a75827bb85b11da72016497fc10dac1106c837ed593969ee8
languageName: node
linkType: hard
"p-locate@npm:^5.0.0": "p-locate@npm:^5.0.0":
version: 5.0.0 version: 5.0.0
resolution: "p-locate@npm:5.0.0" resolution: "p-locate@npm:5.0.0"
@ -7300,6 +7468,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"p-try@npm:^2.0.0":
version: 2.2.0
resolution: "p-try@npm:2.2.0"
checksum: 10c0/c36c19907734c904b16994e6535b02c36c2224d433e01a2f1ab777237f4d86e6289fd5fd464850491e940379d4606ed850c03e0f9ab600b0ebddb511312e177f
languageName: node
linkType: hard
"package-json-from-dist@npm:^1.0.0": "package-json-from-dist@npm:^1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "package-json-from-dist@npm:1.0.0" resolution: "package-json-from-dist@npm:1.0.0"
@ -7353,6 +7528,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"path-exists@npm:^3.0.0":
version: 3.0.0
resolution: "path-exists@npm:3.0.0"
checksum: 10c0/17d6a5664bc0a11d48e2b2127d28a0e58822c6740bde30403f08013da599182289c56518bec89407e3f31d3c2b6b296a4220bc3f867f0911fee6952208b04167
languageName: node
linkType: hard
"path-exists@npm:^4.0.0": "path-exists@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "path-exists@npm:4.0.0" resolution: "path-exists@npm:4.0.0"
@ -7475,6 +7657,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pkg-up@npm:^3.1.0":
version: 3.1.0
resolution: "pkg-up@npm:3.1.0"
dependencies:
find-up: "npm:^3.0.0"
checksum: 10c0/ecb60e1f8e1f611c0bdf1a0b6a474d6dfb51185567dc6f29cdef37c8d480ecba5362e006606bb290519bbb6f49526c403fabea93c3090c20368d98bb90c999ab
languageName: node
linkType: hard
"plist@npm:^3.0.4, plist@npm:^3.0.5": "plist@npm:^3.0.4, plist@npm:^3.0.5":
version: 3.1.0 version: 3.1.0
resolution: "plist@npm:3.1.0" resolution: "plist@npm:3.1.0"
@ -8583,6 +8774,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"require-from-string@npm:^2.0.2":
version: 2.0.2
resolution: "require-from-string@npm:2.0.2"
checksum: 10c0/aaa267e0c5b022fc5fd4eef49d8285086b15f2a1c54b28240fdf03599cbd9c26049fee3eab894f2e1f6ca65e513b030a7c264201e3f005601e80c49fb2937ce2
languageName: node
linkType: hard
"require-in-the-middle@npm:^7.1.1": "require-in-the-middle@npm:^7.1.1":
version: 7.3.0 version: 7.3.0
resolution: "require-in-the-middle@npm:7.3.0" resolution: "require-in-the-middle@npm:7.3.0"
@ -9491,6 +9689,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"type-fest@npm:^2.17.0":
version: 2.19.0
resolution: "type-fest@npm:2.19.0"
checksum: 10c0/a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb
languageName: node
linkType: hard
"typed-array-buffer@npm:^1.0.2": "typed-array-buffer@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "typed-array-buffer@npm:1.0.2" resolution: "typed-array-buffer@npm:1.0.2"