feat: add dark and light theme
This commit is contained in:
@@ -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'
|
||||
}
|
||||
+11
-6
@@ -5,8 +5,9 @@ import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
import AppUpdater from './updater'
|
||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { saveFile } from './event'
|
||||
import AppUpdater from './updater'
|
||||
|
||||
function createWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
@@ -15,6 +16,8 @@ function createWindow() {
|
||||
defaultHeight: 670
|
||||
})
|
||||
|
||||
const theme = appConfig.get('theme') || 'light'
|
||||
|
||||
// Create the browser window.
|
||||
const mainWindow = new BrowserWindow({
|
||||
x: mainWindowState.x,
|
||||
@@ -26,11 +29,7 @@ function createWindow() {
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: {
|
||||
height: 41,
|
||||
color: '#1f1f1f',
|
||||
symbolColor: '#eee'
|
||||
},
|
||||
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
|
||||
trafficLightPosition: { x: 8, y: 12 },
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
@@ -118,6 +117,12 @@ app.whenReady().then(() => {
|
||||
|
||||
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 () => {
|
||||
autoUpdater.logger?.info('触发检查更新')
|
||||
|
||||
Vendored
+1
@@ -12,6 +12,7 @@ declare global {
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
saveFile: (path: string, content: string) => void
|
||||
setTheme: (theme: 'light' | 'dark') => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ const api = {
|
||||
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
||||
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
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<title>Cherry Studio</title>
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<!-- 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' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data:" />
|
||||
</head>
|
||||
|
||||
<body theme-mode="dark">
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
+21
-19
@@ -1,35 +1,37 @@
|
||||
import store, { persistor } from '@renderer/store'
|
||||
import { ConfigProvider } from 'antd'
|
||||
import { Provider } from 'react-redux'
|
||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import { AntdThemeConfig, getAntdLocale } from './config/antd'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
import AntdProvider from './providers/AntdProvider'
|
||||
import { ThemeProvider } from './providers/ThemeProvider'
|
||||
|
||||
function App(): JSX.Element {
|
||||
return (
|
||||
<ConfigProvider theme={AntdThemeConfig} locale={getAntdLocale()}>
|
||||
<Provider store={store}>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</ConfigProvider>
|
||||
<Provider store={store}>
|
||||
<ThemeProvider>
|
||||
<AntdProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<TopViewContainer>
|
||||
<HashRouter>
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
</HashRouter>
|
||||
</TopViewContainer>
|
||||
</PersistGate>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1722099305424') format('woff2'),
|
||||
url('iconfont.woff?t=1722099305424') format('woff'),
|
||||
url('iconfont.ttf?t=1722099305424') format('truetype');
|
||||
src: url('iconfont.woff2?t=1722242729348') format('woff2'),
|
||||
url('iconfont.woff?t=1722242729348') format('woff'),
|
||||
url('iconfont.ttf?t=1722242729348') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,14 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-dark1:before {
|
||||
content: "\e72f";
|
||||
}
|
||||
|
||||
.icon-theme-light:before {
|
||||
content: "\e6b7";
|
||||
}
|
||||
|
||||
.icon-translate_line:before {
|
||||
content: "\e7de";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,11 +2,6 @@
|
||||
@import './markdown.scss';
|
||||
@import './scrollbar.scss';
|
||||
|
||||
// @font-face {
|
||||
// font-family: 'Playwrite';
|
||||
// src: url(../fonts/Playwrite.ttf) format('truetype');
|
||||
// }
|
||||
|
||||
:root {
|
||||
--color-white: #ffffff;
|
||||
--color-white-soft: #f8f8f8;
|
||||
@@ -28,17 +23,22 @@
|
||||
--color-background-soft: var(--color-black-soft);
|
||||
--color-background-mute: var(--color-black-mute);
|
||||
|
||||
--color-primary: #00b96b;
|
||||
--color-primary-soft: #00b96b99;
|
||||
--color-primary-mute: #00b96b33;
|
||||
--color-primary: #135200;
|
||||
--color-primary-soft: #13520099;
|
||||
--color-primary-mute: #13520033;
|
||||
|
||||
--color-text: var(--color-text-1);
|
||||
--color-icon: #ffffff99;
|
||||
--color-icon-white: #ffffff;
|
||||
--color-border: #ffffff20;
|
||||
--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;
|
||||
--sidebar-background: #1f1f1f;
|
||||
|
||||
--navbar-height: 42px;
|
||||
--sidebar-width: 55px;
|
||||
--assistants-width: 245px;
|
||||
@@ -48,6 +48,44 @@
|
||||
--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,
|
||||
*::after {
|
||||
@@ -68,8 +106,18 @@ body {
|
||||
line-height: 1.6;
|
||||
overflow: hidden;
|
||||
background-size: cover;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans',
|
||||
'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
font-family:
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Microsoft YaHei',
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue' sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -97,3 +145,9 @@ body,
|
||||
#inputbar .ant-input {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.chat-nav-dropdown {
|
||||
.ant-dropdown-menu {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.markdown {
|
||||
color: #f1f1f1;
|
||||
color: var(--color-text);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
user-select: text;
|
||||
@@ -33,44 +33,36 @@
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 0.9em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
padding-left: 1.5em;
|
||||
margin: 1em 0;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
li {
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
background: var(--color-scrollbar-thumb);
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
background: var(--color-scrollbar-thumb-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ const Container = styled.div`
|
||||
min-width: var(--sidebar-width);
|
||||
min-height: 100%;
|
||||
-webkit-app-region: drag !important;
|
||||
background-color: #1f1f1f;
|
||||
background-color: var(--sidebar-background);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
padding-top: var(--navbar-height);
|
||||
position: relative;
|
||||
@@ -68,7 +68,7 @@ const AvatarImg = styled.img`
|
||||
height: 28px;
|
||||
background-color: var(--color-background-soft);
|
||||
margin: 5px 0;
|
||||
margin-top: ${isMac ? '16px' : '7px'};
|
||||
margin-top: ${isMac ? '16px' : '9px'};
|
||||
`
|
||||
const MainMenus = styled.div`
|
||||
display: flex;
|
||||
@@ -102,7 +102,7 @@ const Icon = styled.div`
|
||||
font-size: 17px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #ffffff30;
|
||||
background-color: var(--color-background-soft);
|
||||
cursor: pointer;
|
||||
.iconfont,
|
||||
.anticon {
|
||||
@@ -110,7 +110,7 @@ const Icon = styled.div`
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background-color: #ffffff20;
|
||||
background-color: var(--color-background-mute);
|
||||
.iconfont,
|
||||
.anticon {
|
||||
color: var(--color-icon-white);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
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() {
|
||||
const settings = useAppSelector((state) => state.settings)
|
||||
@@ -9,6 +14,9 @@ export function useSettings() {
|
||||
...settings,
|
||||
setSendMessageShortcut(shortcut: SendMessageShortcut) {
|
||||
dispatch(_setSendMessageShortcut(shortcut))
|
||||
},
|
||||
setTheme(theme: ThemeMode) {
|
||||
dispatch(setTheme(theme))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,11 @@ const resources = {
|
||||
'about.feedback.button': 'Feedback',
|
||||
'about.contact.title': '📧 Contact',
|
||||
'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: {
|
||||
title: 'Translation',
|
||||
@@ -334,7 +338,11 @@ const resources = {
|
||||
'about.feedback.button': '反馈',
|
||||
'about.contact.title': '📧 邮件联系',
|
||||
'about.contact.button': '邮件',
|
||||
'proxy.title': '代理地址'
|
||||
'proxy.title': '代理地址',
|
||||
'theme.title': '主题',
|
||||
'theme.dark': '深色主题',
|
||||
'theme.light': '浅色主题',
|
||||
'theme.auto': '跟随系统'
|
||||
},
|
||||
translate: {
|
||||
title: '翻译',
|
||||
|
||||
@@ -2,6 +2,7 @@ import localforage from 'localforage'
|
||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import * as Sentry from '@sentry/electron/renderer'
|
||||
import { isProduction, loadScript } from './utils'
|
||||
import { ThemeMode } from './store/settings'
|
||||
|
||||
async function initSentry() {
|
||||
if (await isProduction()) {
|
||||
@@ -21,12 +22,12 @@ async function initSentry() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function initMermaid() {
|
||||
export async function initMermaid(theme: ThemeMode) {
|
||||
if (!window.mermaid) {
|
||||
await loadScript('https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js')
|
||||
window.mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'dark',
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default',
|
||||
securityLevel: 'loose'
|
||||
})
|
||||
window.mermaid.contentLoaded()
|
||||
|
||||
@@ -116,12 +116,16 @@ const AssistantCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 16px;
|
||||
background-color: #111;
|
||||
border: 0.5px solid #151515;
|
||||
background-color: var(--color-background-soft);
|
||||
border: 0.5px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
const EmojiHeader = styled.div`
|
||||
width: 25px;
|
||||
@@ -148,7 +152,7 @@ const AssistantName = styled(Title)`
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
color: var(--color-white);
|
||||
font-weight: 900;
|
||||
`
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore
|
||||
import Navigation from './components/NavigationCenter'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { Switch } from 'antd'
|
||||
|
||||
let _activeAssistant: Assistant
|
||||
|
||||
@@ -18,6 +20,7 @@ const HomePage: FC = () => {
|
||||
const { rightSidebarShown, toggleRightSidebar } = useShowRightSidebar()
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
@@ -42,6 +45,12 @@ const HomePage: FC = () => {
|
||||
)}
|
||||
<Navigation activeAssistant={activeAssistant} />
|
||||
<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}>
|
||||
<i className={`iconfont ${rightSidebarShown ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
|
||||
</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
|
||||
|
||||
@@ -96,7 +96,9 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
<AssistantItem
|
||||
onClick={() => onSwitchAssistant(assistant)}
|
||||
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>
|
||||
</Dropdown>
|
||||
</div>
|
||||
@@ -143,13 +145,15 @@ const AssistantItem = styled.div`
|
||||
&.active {
|
||||
background-color: var(--color-background-mute);
|
||||
cursor: pointer;
|
||||
.name {
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const AssistantName = styled.div`
|
||||
font-size: 14px;
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react'
|
||||
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 { CheckOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
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 {
|
||||
children: string
|
||||
@@ -15,16 +17,20 @@ interface CodeBlockProps {
|
||||
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) => {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const [copied, setCopied] = useState(false)
|
||||
const { theme } = useTheme()
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onCopy = () => {
|
||||
navigator.clipboard.writeText(children)
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-code' })
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
if (match && match[1] === 'mermaid') {
|
||||
initMermaid()
|
||||
initMermaid(theme)
|
||||
return <Mermaid chart={children} />
|
||||
}
|
||||
|
||||
@@ -32,12 +38,13 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) =
|
||||
<div>
|
||||
<CodeHeader>
|
||||
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
||||
<CopyOutlined className="copy" onClick={onCopy} />
|
||||
{!copied && <CopyOutlined className="copy" onClick={onCopy} />}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
</CodeHeader>
|
||||
<SyntaxHighlighter
|
||||
{...rest}
|
||||
language={match[1]}
|
||||
style={atomDark}
|
||||
style={theme === ThemeMode.dark ? atomDark : oneLight}
|
||||
wrapLongLines={true}
|
||||
customStyle={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, marginTop: 0 }}>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
@@ -54,10 +61,10 @@ const CodeHeader = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
color: #fff;
|
||||
color: var(--color-text);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
background-color: #323232;
|
||||
background-color: var(--color-code-background);
|
||||
height: 40px;
|
||||
padding: 0 10px;
|
||||
border-top-left-radius: 8px;
|
||||
|
||||
@@ -248,7 +248,7 @@ const ToolbarButton = styled(Button)`
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
.anticon {
|
||||
color: white;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
`
|
||||
@@ -260,7 +260,7 @@ const TextCount = styled.div`
|
||||
font-size: 11px;
|
||||
color: var(--color-text-3);
|
||||
z-index: 10;
|
||||
background-color: #121212;
|
||||
background-color: var(--color-background-soft);
|
||||
padding: 2px 8px;
|
||||
border-top-left-radius: 7px;
|
||||
user-select: none;
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { CopyOutlined, DeleteOutlined, EditOutlined, MenuOutlined, SaveOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import Logo from '@renderer/assets/images/logo.png'
|
||||
import {
|
||||
CheckOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
MenuOutlined,
|
||||
SaveOutlined,
|
||||
SyncOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Message } from '@renderer/types'
|
||||
import { firstLetter } from '@renderer/utils'
|
||||
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
|
||||
import { Avatar, Dropdown, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { isEmpty, upperFirst } from 'lodash'
|
||||
import { FC, useCallback } from 'react'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Markdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
@@ -31,6 +38,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
const { assistant } = useAssistant(message.assistantId)
|
||||
const { userName, showMessageDivider, messageFont } = useSettings()
|
||||
const { generating } = useRuntime()
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const isLastMessage = index === 0
|
||||
const isUserMessage = message.role === 'user'
|
||||
@@ -39,6 +47,8 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
const onCopy = () => {
|
||||
navigator.clipboard.writeText(message.content)
|
||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}
|
||||
|
||||
const onDelete = async () => {
|
||||
@@ -105,14 +115,14 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
<MessageHeader>
|
||||
<AvatarWrapper>
|
||||
{message.role === 'assistant' ? (
|
||||
<Avatar src={message.modelId ? getModelLogo(message.modelId) : Logo} size={35}>
|
||||
{firstLetter(message.modelId).toUpperCase()}
|
||||
<Avatar src={message.modelId ? getModelLogo(message.modelId) : undefined} size={35}>
|
||||
{firstLetter(assistant?.name).toUpperCase()}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar src={avatar} size={35} />
|
||||
)}
|
||||
<UserWrap>
|
||||
<UserName>{getUserName()}</UserName>
|
||||
<UserName>{removeLeadingEmoji(getUserName())}</UserName>
|
||||
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
|
||||
</UserWrap>
|
||||
</AvatarWrapper>
|
||||
@@ -137,25 +147,26 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
||||
<ActionButton>
|
||||
<EditOutlined onClick={onEdit} />
|
||||
<ActionButton onClick={onEdit}>
|
||||
<EditOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||
<ActionButton>
|
||||
<CopyOutlined onClick={onCopy} />
|
||||
<ActionButton onClick={onCopy}>
|
||||
{!copied && <CopyOutlined />}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
|
||||
<ActionButton>
|
||||
<DeleteOutlined onClick={onDelete} />
|
||||
<ActionButton onClick={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{canRegenerate && (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton>
|
||||
<SyncOutlined onClick={onRegenerate} />
|
||||
<ActionButton onClick={onRegenerate}>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CodeSandboxOutlined } from '@ant-design/icons'
|
||||
import { NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { colorPrimary } from '@renderer/config/antd'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
@@ -13,6 +12,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
import { NewButton } from '../HomePage'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { removeLeadingEmoji } from '@renderer/utils'
|
||||
|
||||
interface Props {
|
||||
activeAssistant: Assistant
|
||||
@@ -34,7 +34,7 @@ const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
|
||||
children: p.models.map((m) => ({
|
||||
key: m.id,
|
||||
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} />,
|
||||
onClick: () => setModel(m)
|
||||
}))
|
||||
@@ -47,8 +47,11 @@ const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
|
||||
<i className="iconfont icon-showsidebarhoriz" />
|
||||
</NewButton>
|
||||
)}
|
||||
<AssistantName>{assistant?.name || t('assistant.default.name')}</AssistantName>
|
||||
<DropdownMenu menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']}>
|
||||
<AssistantName>{removeLeadingEmoji(assistant?.name) || t('assistant.default.name')}</AssistantName>
|
||||
<DropdownMenu
|
||||
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }}
|
||||
trigger={['click']}
|
||||
overlayClassName="chat-nav-dropdown">
|
||||
<DropdownButton size="small" type="primary" ghost>
|
||||
<CodeSandboxOutlined />
|
||||
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
|
||||
@@ -69,13 +72,14 @@ const AssistantName = styled.span`
|
||||
`
|
||||
|
||||
const DropdownButton = styled(Button)`
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
border-radius: 15px;
|
||||
padding: 0 8px;
|
||||
`
|
||||
|
||||
const ModelName = styled.span`
|
||||
margin-left: -2px;
|
||||
font-weight: bolder;
|
||||
`
|
||||
|
||||
export default NavigationCenter
|
||||
|
||||
@@ -90,10 +90,10 @@ const Tab = styled.div`
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
color: #8a8a8a;
|
||||
color: var(--color-text-3);
|
||||
border-bottom: 1px solid transparent;
|
||||
&.active {
|
||||
color: #bbb;
|
||||
color: var(--color-text-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -130,7 +130,7 @@ const TopicListItem = styled.div`
|
||||
margin-bottom: 5px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -138,7 +138,8 @@ const TopicListItem = styled.div`
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-background-soft);
|
||||
background-color: var(--color-background-mute);
|
||||
font-weight: bolder;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -8,14 +8,14 @@ import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar } from '@renderer/store/runtime'
|
||||
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 { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
const GeneralSettings: FC = () => {
|
||||
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 dispatch = useAppDispatch()
|
||||
const { t } = useTranslation()
|
||||
@@ -53,6 +53,20 @@ const GeneralSettings: FC = () => {
|
||||
/>
|
||||
</SettingRow>
|
||||
<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>
|
||||
<SettingRowTitle>{t('common.avatar')}</SettingRowTitle>
|
||||
<Upload
|
||||
|
||||
@@ -100,11 +100,11 @@ const ProviderSettings: FC = () => {
|
||||
key={JSON.stringify(provider)}
|
||||
className={provider.id === selectedProvider?.id ? 'active' : ''}
|
||||
onClick={() => setSelectedProvider(provider)}>
|
||||
{provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={28} />}
|
||||
{provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={25} />}
|
||||
{!provider.isSystem && (
|
||||
<Avatar
|
||||
size={28}
|
||||
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 28 }}>
|
||||
size={25}
|
||||
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 25 }}>
|
||||
{getFirstCharacter(provider.name)}
|
||||
</Avatar>
|
||||
)}
|
||||
@@ -151,7 +151,7 @@ const ProviderListContainer = styled.div`
|
||||
width: var(--assistants-width);
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
padding: 10px 8px;
|
||||
padding: 8px;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
@@ -173,20 +173,21 @@ const ProviderListItem = styled.div`
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: #135200;
|
||||
background: var(--color-primary-mute);
|
||||
}
|
||||
&.active {
|
||||
background: #135200;
|
||||
font-weight: bold;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
font-weight: bold !important;
|
||||
}
|
||||
`
|
||||
|
||||
const ProviderItemName = styled.div`
|
||||
margin-left: 10px;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const AddButtonWrapper = styled.div`
|
||||
|
||||
@@ -68,7 +68,7 @@ const SettingMenus = styled.ul`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: var(--assistants-width);
|
||||
border-right: 1px solid var(--color-border);
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
padding: 10px;
|
||||
`
|
||||
|
||||
@@ -84,13 +84,14 @@ const MenuItem = styled.li`
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background: #135200;
|
||||
background: var(--color-primary-soft);
|
||||
}
|
||||
&.active {
|
||||
background: #135200;
|
||||
font-weight: bold;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -178,7 +178,8 @@ const ListHeader = styled.div`
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-background-soft);
|
||||
padding: 8px 22px;
|
||||
color: #ffffff50;
|
||||
color: var(--color-white);
|
||||
opacity: 0.4;
|
||||
`
|
||||
|
||||
const ListItem = styled.div`
|
||||
@@ -199,14 +200,14 @@ const ListItemHeader = styled.div`
|
||||
`
|
||||
|
||||
const ListItemName = styled.div`
|
||||
color: #fff;
|
||||
color: var(--color-white);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-left: 6px;
|
||||
`
|
||||
|
||||
const ModelHeaderTitle = styled.div`
|
||||
color: #fff;
|
||||
color: var(--color-white);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-right: 10px;
|
||||
|
||||
@@ -20,6 +20,7 @@ import Link from 'antd/es/typography/Link'
|
||||
import { checkApi } from '@renderer/services/api'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
|
||||
interface Props {
|
||||
provider: Provider
|
||||
@@ -33,6 +34,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
const [apiChecking, setApiChecking] = useState(false)
|
||||
const { updateProvider, models, removeModel } = useProvider(provider.id)
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
|
||||
const modelGroups = groupBy(models, 'group')
|
||||
|
||||
@@ -68,13 +70,18 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingContainer
|
||||
style={
|
||||
theme === 'dark'
|
||||
? { backgroundColor: 'var(--color-background)' }
|
||||
: { backgroundColor: 'var(--color-background-mute)' }
|
||||
}>
|
||||
<SettingTitle>
|
||||
<Flex align="center">
|
||||
<span>{provider.isSystem ? t(`provider.${provider.id}`) : provider.name}</span>
|
||||
{officialWebsite! && (
|
||||
<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>
|
||||
)}
|
||||
</Flex>
|
||||
@@ -183,7 +190,8 @@ const HelpTextRow = styled.div`
|
||||
|
||||
const HelpText = styled.div`
|
||||
font-size: 11px;
|
||||
color: #ffffff50;
|
||||
color: var(--color-text);
|
||||
opacity: 0.4;
|
||||
`
|
||||
|
||||
const HelpLink = styled(Link)`
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -19,7 +19,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 16,
|
||||
version: 17,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -261,6 +261,15 @@ const migrateConfig = {
|
||||
showInputEstimatedTokens: false
|
||||
}
|
||||
}
|
||||
},
|
||||
'17': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
theme: 'auto'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export type SendMessageShortcut = 'Enter' | 'Shift+Enter'
|
||||
|
||||
export enum ThemeMode {
|
||||
light = 'light',
|
||||
dark = 'dark',
|
||||
auto = 'auto'
|
||||
}
|
||||
|
||||
export interface SettingsState {
|
||||
showRightSidebar: boolean
|
||||
showAssistants: boolean
|
||||
@@ -12,6 +18,7 @@ export interface SettingsState {
|
||||
showMessageDivider: boolean
|
||||
messageFont: 'system' | 'serif'
|
||||
showInputEstimatedTokens: boolean
|
||||
theme: ThemeMode
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
@@ -23,7 +30,8 @@ const initialState: SettingsState = {
|
||||
userName: '',
|
||||
showMessageDivider: true,
|
||||
messageFont: 'system',
|
||||
showInputEstimatedTokens: false
|
||||
showInputEstimatedTokens: false,
|
||||
theme: ThemeMode.light
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
@@ -59,6 +67,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setShowInputEstimatedTokens: (state, action: PayloadAction<boolean>) => {
|
||||
state.showInputEstimatedTokens = action.payload
|
||||
},
|
||||
setTheme: (state, action: PayloadAction<ThemeMode>) => {
|
||||
state.theme = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -73,7 +84,8 @@ export const {
|
||||
setUserName,
|
||||
setShowMessageDivider,
|
||||
setMessageFont,
|
||||
setShowInputEstimatedTokens
|
||||
setShowInputEstimatedTokens,
|
||||
setTheme
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@@ -93,9 +93,14 @@ export function droppableReorder<T>(list: T[], startIndex: number, endIndex: num
|
||||
return result
|
||||
}
|
||||
|
||||
// firstLetter
|
||||
export const firstLetter = (str?: string) => {
|
||||
return str ? str[0] : ''
|
||||
export function firstLetter(str: string): string {
|
||||
const match = str.match(/\p{L}\p{M}*|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/u)
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user