Compare commits

...

31 Commits

Author SHA1 Message Date
kangfenmao
29d6d607da chore(version): 0.4.4 2024-07-31 13:54:04 +08:00
kangfenmao
e64375a74c feat(Inputbar.tsx): change height to min-height for Inputbar 2024-07-31 13:41:02 +08:00
kangfenmao
4689bb53e9 chore(package.json): add publish script to automate the release and patch version push process 2024-07-31 13:11:31 +08:00
kangfenmao
e00c66e54a chore(version): 0.4.3 2024-07-31 13:08:19 +08:00
kangfenmao
62b0908dfa feat: add send message button 2024-07-31 13:07:02 +08:00
kangfenmao
cb0b9de1e9 feat: default enable new added provider 2024-07-31 12:21:46 +08:00
kangfenmao
d8d4afbc0d feat: add message suggestions 2024-07-31 12:13:03 +08:00
kangfenmao
c50ff4585a chore(version): 0.4.2 2024-07-30 17:53:45 +08:00
kangfenmao
a5ee8548f3 feat(AboutSettings): implement functionality to open license page from about settings 2024-07-30 16:33:58 +08:00
kangfenmao
15b286a095 doc: update LICENSE 2024-07-30 16:13:32 +08:00
kangfenmao
d47d4a158d docs: change offical website url 2024-07-30 15:31:17 +08:00
kangfenmao
cd85dcddf8 remove: website 2024-07-30 15:30:35 +08:00
kangfenmao
925a9fb8ec fix: delete provider crash 2024-07-30 15:30:09 +08:00
kangfenmao
17c3437e02 chore(version): 0.4.1 2024-07-29 18:18:03 +08:00
kangfenmao
69293846fc fix: model list text color 2024-07-29 18:17:50 +08:00
kangfenmao
20a7fbfc48 fix(ProviderSDK.ts): translation message 2024-07-29 17:45:08 +08:00
kangfenmao
64d4b8450a style(website): adjust border-radius of images to 20% 2024-07-29 17:36:27 +08:00
kangfenmao
f080fc5048 chore(version): 0.4.0 2024-07-29 17:33:09 +08:00
kangfenmao
50f08124d7 feat: add dark and light theme 2024-07-29 17:14:49 +08:00
kangfenmao
b91081ef99 docs(index.html): update website URLs from easys.run to cherry-ai.com 2024-07-29 09:55:24 +08:00
kangfenmao
d869ec9a9b chore(version): 0.3.9 2024-07-29 09:16:46 +08:00
kangfenmao
70c4354d6c feat: add model logo on select model dropdown 2024-07-28 15:10:36 +08:00
kangfenmao
527c4e77dc fix(Message.tsx): add optional chaining to assistant.name to prevent potential undefined errors 2024-07-28 11:16:16 +08:00
kangfenmao
2483ce3bb4 chore(version): 0.3.8 2024-07-28 02:28:48 +08:00
kangfenmao
db3f8b8bee refactor(TranslatePage.tsx): simplify OutputText styling for cleaner code structure 2024-07-28 02:28:48 +08:00
kangfenmao
45bf3d4e86 fix(index.html): update Content-Security-Policy to allow fonts 2024-07-28 01:37:43 +08:00
kangfenmao
59b39dc41a feat(TranslatePage.tsx): add markdown style to handle whitespace properly in translation output 2024-07-28 01:32:03 +08:00
kangfenmao
a267a8d4c3 feat: add translation module 2024-07-28 01:07:15 +08:00
kangfenmao
5b123f2c33 fix(markdown.scss): replace :first-of-type with :first-child for consistent styling of first elements 2024-07-26 18:02:50 +08:00
kangfenmao
fe34fb3c25 fix(api.ts): modify provider apiKey check to exclude 'ollama' provider 2024-07-26 18:02:32 +08:00
kangfenmao
e6359d2048 feat(markdown.scss): add white-space: pre-wrap to code elements 2024-07-26 17:22:48 +08:00
58 changed files with 1556 additions and 468 deletions

114
LICENSE
View File

@@ -1,21 +1,101 @@
MIT License
### Cherry Studio 商业许可协议
Copyright (c) 2024 亢奋猫
---
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
#### 中文版
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
**Cherry Studio 商业许可协议**
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
本协议(以下简称“协议”)由以下双方签订:
- 许可方王谦kangfenmao@qq.com
- 被许可方:[被许可方名称]
**1. 定义**
- “软件”指 Cherry Studio 软件,网址为 https://cherry-ai.com。
- “商业用途”指任何以盈利为目的的使用。
**2. 许可**
- 未经许可方明确书面许可,被许可方不得将软件用于商业用途。
- 未经许可方事先书面同意,被许可方不得将软件全部或部分用于商业用途分发。
- 未经许可方明确授权,被许可方不得再许可、租赁、销售、出租或以其他方式将软件转让给任何第三方用于商业用途。
**3. 责任限制**
开发者不对因使用本软件而产生的任何直接或间接损失承担责任。用户应自行承担使用本软件的风险。
**4. 许可协议生效日期**
本许可协议自用户首次下载或使用本软件之日起生效。
**5. 许可终止**
如发现用户违反上述条款,开发者有权随时终止本许可,并要求用户停止使用本软件及删除所有相关副本。
**6. 其他**
本协议的解释、效力及争议的解决,均适用中华人民共和国法律。
**7. 联系信息**
- 许可方联系方式:
- 手机号18539907620
- 邮箱kangfenmao@qq.com
**许可方(签字):**
**日期:**
**被许可方(签字):**
**日期:**
---
#### English Version
**Cherry Studio Commercial License Agreement**
This Agreement ("Agreement") is entered into by and between:
- Licensor: Wang Qian (kangfenmao)
- Licensee: [Licensee Name]
**1. Definitions**
- "Software" refers to the Cherry Studio software, available at https://cherry-ai.com.
- "Commercial Use" refers to any use for profit.
**2. License**
- The Licensee may not use the Software for Commercial Use without the Licensor's explicit written permission.
- The Licensee may not distribute the Software in whole or in part for Commercial Use without the Licensor's prior written consent.
- The Licensee may not sublicense, lease, sell, rent, or otherwise transfer the Software to any third party for Commercial Use without the Licensor's explicit authorization.
**3. Termination of License**
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
**4. Effective Date of License Agreement**
This license agreement becomes effective from the date the user first downloads or uses the software.
**5. Termination of License**
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
**6. Miscellaneous**
This Agreement shall be governed by and construed in accordance with the laws of the People's Republic of China.
**7. Contact Information**
- Licensor's Contact Details:
- Phone: 18539907620
- Email: kangfenmao@qq.com
**Licensor (Signature):**
**Date:**
**Licensee (Signature):**
**Date:**

View File

@@ -56,8 +56,5 @@ electronDownload:
afterSign: scripts/notarize.js
releaseInfo:
releaseNotes: |
1. 保存聊天页面状态,切换页面后恢复
2. 修复默认助手名称为空时候的显示问题
3. 简化系统内置智能体提示词长度
4. 增加单个聊天内容保存到本地功能
5. 系统内置提供商支持修改 API 地址
新增发送按钮
输入区域展开可以全屏显示

View File

@@ -1,6 +1,6 @@
{
"name": "cherry-studio",
"version": "0.3.7",
"version": "0.4.4",
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
"author": "kangfenmao@qq.com",
@@ -19,13 +19,15 @@
"build:win": "dotenv npm run build && electron-builder --win --publish never",
"build:mac": "dotenv electron-vite build && electron-builder --mac --publish never",
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
"release": "node scripts/version.js"
"release": "node scripts/version.js",
"publish": "yarn release patch push"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"@sentry/electron": "^5.2.0",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.1.7",
"electron-window-state": "^5.0.3"
},
@@ -69,6 +71,7 @@
"react-redux": "^9.1.2",
"react-router": "6",
"react-router-dom": "6",
"react-spinners": "^0.14.1",
"react-syntax-highlighter": "^15.5.0",
"redux-persist": "^6.0.0",
"sass": "^1.77.2",

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 { 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('触发检查更新')

View File

@@ -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
}
}
}

View File

@@ -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

View File

@@ -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' *; 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>
<body theme-mode="dark">
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>

View File

@@ -1,33 +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="/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>
)
}

View File

@@ -0,0 +1,55 @@
@font-face {
font-family: "iconfont"; /* Project id 4563475 */
src: url('iconfont.woff2?t=1722242729348') format('woff2'),
url('iconfont.woff?t=1722242729348') format('woff'),
url('iconfont.ttf?t=1722242729348') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-dark1:before {
content: "\e72f";
}
.icon-theme-light:before {
content: "\e6b7";
}
.icon-translate_line:before {
content: "\e7de";
}
.icon-history:before {
content: "\e758";
}
.icon-hidesidebarhoriz:before {
content: "\e8eb";
}
.icon-showsidebarhoriz:before {
content: "\e944";
}
.icon-a-addchat:before {
content: "\e658";
}
.icon-appstore:before {
content: "\e792";
}
.icon-chat:before {
content: "\e615";
}
.icon-setting:before {
content: "\e78e";
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,12 +1,7 @@
@import 'https://at.alicdn.com/t/c/font_4563475_hrx8c92awui.css';
@import '../fonts/icon-fonts/iconfont.css';
@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,24 +23,67 @@
--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;
--topic-list-width: 260px;
--settings-width: var(--assistants-width);
--status-bar-height: 40px;
--input-bar-height: 125px;
--input-bar-height: 135px;
}
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;
}
*,
@@ -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,21 @@ body,
#inputbar .ant-input {
resize: none;
}
.chat-nav-dropdown {
.ant-dropdown-menu {
padding-bottom: 12px;
}
}
.loader {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #000;
box-shadow:
32px 0 #000,
-32px 0 #000;
position: relative;
animation: flash 0.5s ease-out infinite alternate;
}

View File

@@ -1,5 +1,5 @@
.markdown {
color: #f1f1f1;
color: var(--color-text);
font-size: 15px;
line-height: 1.6;
user-select: text;
@@ -8,16 +8,16 @@
margin-bottom: 5px;
}
p:first-of-type {
p:first-child {
margin-top: 0;
}
h1:first-of-type,
h2:first-of-type,
h3:first-of-type,
h4:first-of-type,
h5:first-of-type,
h6:first-of-type {
h1:first-child,
h2:first-child,
h3:first-child,
h4:first-child,
h5:first-child,
h6:first-child {
margin-top: 0;
}
@@ -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 {
@@ -87,4 +79,8 @@
span {
word-break: break-all;
}
code {
white-space: pre-wrap;
}
}

View File

@@ -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);
}
}

View File

@@ -4,6 +4,7 @@ import styled from 'styled-components'
import { Link, useLocation } from 'react-router-dom'
import useAvatar from '@renderer/hooks/useAvatar'
import { isMac, isWindows } from '@renderer/config/constant'
import { TranslationOutlined } from '@ant-design/icons'
const Sidebar: FC = () => {
const { pathname } = useLocation()
@@ -29,6 +30,11 @@ const Sidebar: FC = () => {
<i className="iconfont icon-appstore"></i>
</Icon>
</StyledLink>
<StyledLink to="/translate">
<Icon className={isRoute('/translate')}>
<TranslationOutlined />
</Icon>
</StyledLink>
</Menus>
</MainMenus>
<Menus>
@@ -50,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;
@@ -62,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;
@@ -85,22 +91,28 @@ const Icon = styled.div`
margin-bottom: 5px;
transition: background-color 0.2s ease;
-webkit-app-region: none;
.iconfont {
.iconfont,
.anticon {
color: var(--color-icon);
font-size: 20px;
transition: color 0.2s ease;
text-decoration: none;
}
.anticon {
font-size: 17px;
}
&:hover {
background-color: #ffffff30;
background-color: var(--color-background-soft);
cursor: pointer;
.iconfont {
.iconfont,
.anticon {
color: var(--color-icon-white);
}
}
&.active {
background-color: #ffffff20;
.iconfont {
background-color: var(--color-background-mute);
.iconfont,
.anticon {
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

@@ -14,7 +14,7 @@ import {
updateTopic,
updateTopics
} from '@renderer/store/assistants'
import { setDefaultModel as _setDefaultModel, setTopicNamingModel as _setTopicNamingModel } from '@renderer/store/llm'
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import localforage from 'localforage'
@@ -71,13 +71,15 @@ export function useDefaultAssistant() {
}
export function useDefaultModel() {
const { defaultModel, topicNamingModel } = useAppSelector((state) => state.llm)
const { defaultModel, topicNamingModel, translateModel } = useAppSelector((state) => state.llm)
const dispatch = useAppDispatch()
return {
defaultModel,
topicNamingModel,
setDefaultModel: (model: Model) => dispatch(_setDefaultModel({ model })),
setTopicNamingModel: (model: Model) => dispatch(_setTopicNamingModel({ model }))
translateModel,
setDefaultModel: (model: Model) => dispatch(setDefaultModel({ model })),
setTopicNamingModel: (model: Model) => dispatch(setTopicNamingModel({ model })),
setTranslateModel: (model: Model) => dispatch(setTranslateModel({ model }))
}
}

View File

@@ -47,7 +47,7 @@ export function useProvider(id: string) {
return {
provider,
models: provider.models,
models: provider?.models || [],
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
addModel: (model: Model) => dispatch(addModel({ providerId: id, model })),
removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model }))

View File

@@ -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))
}
}
}

View File

@@ -78,7 +78,8 @@ const resources = {
'settings.conext_count.tip': 'The number of previous messages to keep in the context.',
'settings.reset': 'Reset',
'settings.set_as_default': 'Apply to default assistant',
'settings.max': 'Max'
'settings.max': 'Max',
'suggestions.title': 'Suggested Questions'
},
apps: {
title: 'Agents'
@@ -102,7 +103,7 @@ const resources = {
title: 'Settings',
general: 'General Settings',
provider: 'Model Provider',
model: 'Model Settings',
model: 'Default Model',
assistant: 'Default Assistant',
about: 'About & Feedback',
'messages.model.title': 'Model Settings',
@@ -124,6 +125,7 @@ const resources = {
'provider.api.url.reset': 'Reset',
'models.default_assistant_model': 'Default Assistant Model',
'models.topic_naming_model': 'Topic Naming Model',
'models.translate_model': 'Translate Model',
'models.add.add_model': 'Add Model',
'models.add.model_id.placeholder': 'Required e.g. gpt-3.5-turbo',
'models.add.model_id': 'Model ID',
@@ -154,8 +156,35 @@ const resources = {
'about.feedback.title': '📝 Feedback',
'about.feedback.button': 'Feedback',
'about.contact.title': '📧 Contact',
'about.license.title': '📄 License',
'about.license.button': 'License',
'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',
'any.language': 'Any language',
'button.translate': 'Translate',
'error.not_configured': 'Translation model is not configured',
'input.placeholder': 'Enter text to translate',
'output.placeholder': 'Translation'
},
languages: {
english: 'English',
chinese: 'Chinese',
'chinese-traditional': 'Traditional Chinese',
japanese: 'Japanese',
korean: 'Korean',
russian: 'Russian',
spanish: 'Spanish',
french: 'French',
italian: 'Italian',
portuguese: 'Portuguese',
arabic: 'Arabic'
}
}
},
@@ -189,7 +218,7 @@ const resources = {
select_model: '选择模型'
},
message: {
copied: '已复制!',
copied: '已复制',
'assistant.added.content': '智能体添加成功',
'message.delete.title': '删除消息',
'message.delete.content': '确定要删除此消息吗?',
@@ -234,7 +263,8 @@ const resources = {
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10代码生成建议 5-10',
'settings.reset': '重置',
'settings.set_as_default': '应用到默认助手',
'settings.max': '不限'
'settings.max': '不限',
'suggestions.title': '建议的问题'
},
apps: {
title: '智能体'
@@ -258,7 +288,7 @@ const resources = {
title: '设置',
general: '常规设置',
provider: '模型提供商',
model: '模型设置',
model: '默认模型',
assistant: '默认助手',
about: '关于我们',
'messages.model.title': '模型设置',
@@ -280,6 +310,7 @@ const resources = {
'provider.api.url.reset': '重置',
'models.default_assistant_model': '默认助手模型',
'models.topic_naming_model': '话题命名模型',
'models.translate_model': '翻译模型',
'models.add.add_model': '添加模型',
'models.add.model_id.placeholder': '必填 例如 gpt-3.5-turbo',
'models.add.model_id': '模型 ID',
@@ -310,8 +341,35 @@ const resources = {
'about.feedback.title': '📝 意见反馈',
'about.feedback.button': '反馈',
'about.contact.title': '📧 邮件联系',
'about.license.title': '📄 许可证',
'about.license.button': '查看',
'about.contact.button': '邮件',
'proxy.title': '代理地址'
'proxy.title': '代理地址',
'theme.title': '主题',
'theme.dark': '深色主题',
'theme.light': '浅色主题',
'theme.auto': '跟随系统'
},
translate: {
title: '翻译',
'any.language': '任意语言',
'button.translate': '翻译',
'error.not_configured': '翻译模型未配置',
'input.placeholder': '输入文本进行翻译',
'output.placeholder': '翻译'
},
languages: {
english: '英文',
chinese: '简体中文',
'chinese-traditional': '繁体中文',
japanese: '日文',
korean: '韩文',
russian: '俄文',
spanish: '西班牙文',
french: '法文',
italian: '意大利文',
portuguese: '葡萄牙文',
arabic: '阿拉伯文'
}
}
}

View File

@@ -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()

View File

@@ -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;
`

View File

@@ -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
@@ -27,6 +30,8 @@ const HomePage: FC = () => {
setActiveAssistant(assistant)
}
console.debug('theme', theme)
return (
<Container>
<Navbar>
@@ -42,6 +47,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" />}
checked={theme === 'dark'}
onChange={toggleTheme}
/>
<NewButton onClick={toggleRightSidebar}>
<i className={`iconfont ${rightSidebarShown ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
</NewButton>
@@ -101,4 +112,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

View File

@@ -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;

View File

@@ -18,10 +18,10 @@ const Chat: FC<Props> = (props) => {
return (
<Container id="chat">
<Flex vertical flex={1} justify="space-between">
<Main vertical flex={1} justify="space-between">
<Messages assistant={assistant} topic={activeTopic} />
<Inputbar assistant={assistant} setActiveTopic={setActiveTopic} />
</Flex>
</Main>
<RightSidebar assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
</Container>
)
@@ -35,4 +35,8 @@ const Container = styled.div`
justify-content: space-between;
`
const Main = styled(Flex)`
height: calc(100vh - var(--navbar-height));
`
export default Chat

View File

@@ -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;

View File

@@ -4,10 +4,10 @@ import {
FullscreenExitOutlined,
FullscreenOutlined,
HistoryOutlined,
MoreOutlined,
PauseCircleOutlined,
PlusCircleOutlined
} from '@ant-design/icons'
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import { getDefaultTopic } from '@renderer/services/assistant'
@@ -23,8 +23,7 @@ import { debounce, isEmpty } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import SendMessageSetting from './SendMessageSetting'
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
import SendMessageButton from './SendMessageButton'
interface Props {
assistant: Assistant
@@ -63,11 +62,25 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
setText('')
setExpend(false)
}
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (expended) {
if (event.key === 'Escape') {
setExpend(false)
return
}
if (event.key === 'Enter' && event.shiftKey) {
sendMessage()
return
}
return
}
if (sendMessageShortcut === 'Enter' && event.key === 'Enter') {
if (event.shiftKey) {
return
@@ -127,7 +140,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}, [assistant])
return (
<Container id="inputbar" style={{ minHeight: expended ? '35%' : 'var(--input-bar-height)' }}>
<Container id="inputbar" style={{ minHeight: expended ? '100%' : 'var(--input-bar-height)' }}>
<Toolbar>
<ToolbarMenu>
<Tooltip placement="top" title={t('assistant.input.new_chat')} arrow>
@@ -158,11 +171,6 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<ControlOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={expended ? t('assistant.input.collapse') : t('assistant.input.expand')} arrow>
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
</Tooltip>
</ToolbarMenu>
<ToolbarMenu>
{generating && (
@@ -172,11 +180,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</ToolbarButton>
</Tooltip>
)}
<SendMessageSetting>
<ToolbarButton type="text" style={{ marginRight: 0 }}>
<MoreOutlined />
<Tooltip placement="top" title={expended ? t('assistant.input.collapse') : t('assistant.input.expand')} arrow>
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
</SendMessageSetting>
</Tooltip>
</ToolbarMenu>
</Toolbar>
<Textarea
@@ -187,16 +195,18 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
autoFocus
contextMenu="true"
variant="borderless"
showCount
ref={inputRef}
styles={{ textarea: { paddingLeft: 0 } }}
/>
{showInputEstimatedTokens && (
<TextCount>
<HistoryOutlined /> {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} | T
{`${inputTokenCount}/${estimateTokenCount}`}
</TextCount>
)}
<Footer>
{showInputEstimatedTokens && (
<TextCount>
<HistoryOutlined /> {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} | T
{`${inputTokenCount}/${estimateTokenCount}`}
</TextCount>
)}
<SendMessageButton sendMessage={sendMessage} />
</Footer>
</Container>
)
}
@@ -225,6 +235,7 @@ const Toolbar = styled.div`
justify-content: space-between;
margin: 0 -5px;
margin-bottom: 5px;
margin-right: -8px;
`
const ToolbarMenu = styled.div`
@@ -248,22 +259,27 @@ const ToolbarButton = styled(Button)`
&:hover {
background-color: var(--color-background-soft);
.anticon {
color: white;
color: var(--color-text-1);
}
}
`
const Footer = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
margin-bottom: 8px;
`
const TextCount = styled.div`
position: absolute;
right: 0;
bottom: 0;
font-size: 11px;
color: var(--color-text-3);
z-index: 10;
background-color: #121212;
padding: 2px 8px;
border-top-left-radius: 7px;
user-select: none;
margin-right: 10px;
`
export default Inputbar

View File

@@ -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 () => {
@@ -70,7 +80,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const getUserName = useCallback(() => {
if (message.id === 'assistant') {
return assistant.name
return assistant?.name
}
if (message.role === 'assistant') {
@@ -78,7 +88,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
}
return userName || t('common.you')
}, [assistant.name, message.id, message.modelId, message.role, t, userName])
}, [assistant?.name, message.id, message.modelId, message.role, t, userName])
const getDropdownMenus = useCallback(
(message: Message) => {
@@ -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>
)}

View File

@@ -4,13 +4,14 @@ import localforage from 'localforage'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
import MessageItem from './Message'
import { reverse } from 'lodash'
import { debounce, reverse } from 'lodash'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
import LocalStorage from '@renderer/services/storage'
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
import { t } from 'i18next'
import Suggestions from './Suggestions'
interface Props {
assistant: Assistant
@@ -93,9 +94,18 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
})
}, [topic.id])
const scrollTop = useCallback(
debounce(() => containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' }), 500, {
leading: true,
trailing: false
}),
[]
)
useEffect(() => {
containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' })
}, [messages])
setTimeout(scrollTop, 100)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messages, lastMessage])
useEffect(() => {
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
@@ -103,6 +113,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
return (
<Container id="messages" key={assistant.id} ref={containerRef}>
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
{lastMessage && <MessageItem message={lastMessage} />}
{reverse([...messages]).map((message, index) => (
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
@@ -118,8 +129,6 @@ const Container = styled.div`
overflow-y: auto;
flex-direction: column-reverse;
max-height: calc(100vh - var(--input-bar-height) - var(--navbar-height));
padding-top: 10px;
padding-bottom: 10px;
.message:first-child {
border: none;
}

View File

@@ -1,17 +1,18 @@
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'
import { useShowAssistants } from '@renderer/hooks/useStore'
import { Assistant } from '@renderer/types'
import { Button, Dropdown, MenuProps } from 'antd'
import { Avatar, Button, Dropdown, MenuProps } from 'antd'
import { upperFirst } from 'lodash'
import { FC } from 'react'
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
@@ -33,7 +34,8 @@ 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)
}))
}))
@@ -45,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>
@@ -67,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

View File

@@ -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;
}
`

View File

@@ -1,12 +1,15 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { Dropdown, MenuProps } from 'antd'
import { FC, PropsWithChildren } from 'react'
import { FC } from 'react'
import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import { DownOutlined } from '@ant-design/icons'
interface Props extends PropsWithChildren {}
interface Props {
sendMessage: () => void
}
const SendMessageSetting: FC<Props> = ({ children }) => {
const SendMessageButton: FC<Props> = ({ sendMessage }) => {
const { sendMessageShortcut, setSendMessageShortcut } = useSettings()
const { t } = useTranslation()
@@ -26,14 +29,15 @@ const SendMessageSetting: FC<Props> = ({ children }) => {
]
return (
<Dropdown
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
placement="topRight"
<Dropdown.Button
onClick={sendMessage}
trigger={['click']}
arrow>
{children}
</Dropdown>
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
icon={<DownOutlined />}
style={{ width: 'auto' }}>
{t('assistant.input.send')}
</Dropdown.Button>
)
}
export default SendMessageSetting
export default SendMessageButton

View File

@@ -0,0 +1,119 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Message, Suggestion } from '@renderer/types'
import { uuid } from '@renderer/utils'
import dayjs from 'dayjs'
import { FC, useEffect, useState } from 'react'
import styled from 'styled-components'
import BeatLoader from 'react-spinners/BeatLoader'
import { fetchSuggestions } from '@renderer/services/api'
interface Props {
assistant: Assistant
messages: Message[]
lastMessage: Message | null
}
const suggestionsMap = new Map<string, Suggestion[]>()
const Suggestions: FC<Props> = ({ assistant, messages, lastMessage }) => {
const [suggestions, setSuggestions] = useState<Suggestion[]>(
suggestionsMap.get(messages[messages.length - 1]?.id) || []
)
const [loadingSuggestions, setLoadingSuggestions] = useState(false)
const onClick = (s: Suggestion) => {
const message: Message = {
id: uuid(),
role: 'user',
content: s.content,
assistantId: assistant.id,
topicId: assistant.topics[0].id || uuid(),
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
status: 'success'
}
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
}
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => {
setLoadingSuggestions(true)
const _suggestions = await fetchSuggestions({ assistant, messages: [...messages, msg] })
if (_suggestions.length) {
setSuggestions(_suggestions)
suggestionsMap.set(msg.id, _suggestions)
}
setLoadingSuggestions(false)
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [assistant, messages])
useEffect(() => {
setSuggestions(suggestionsMap.get(messages[messages.length - 1]?.id) || [])
}, [messages])
if (lastMessage) {
return null
}
if (loadingSuggestions) {
return (
<Container>
<BeatLoader color="var(--color-text-2)" size="10" />
</Container>
)
}
if (suggestions.length === 0) {
return null
}
return (
<Container>
<SuggestionsContainer>
{suggestions.map((s, i) => (
<SuggestionItem key={i} onClick={() => onClick(s)}>
{s.content}
</SuggestionItem>
))}
</SuggestionsContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
padding: 20px;
display: flex;
width: 100%;
flex-direction: row;
flex-wrap: wrap;
gap: 15px;
padding-left: 55px;
`
const SuggestionsContainer = styled.div`
display: flex;
flex-direction: column;
gap: 15px;
`
const SuggestionItem = styled.div`
display: flex;
align-items: center;
width: fit-content;
padding: 7px 15px;
border-radius: 12px;
font-size: 13px;
color: var(--color-text);
background: var(--color-background-mute);
cursor: pointer;
&:hover {
opacity: 0.9;
}
`
export default Suggestions

View File

@@ -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;
}
`

View File

@@ -39,6 +39,10 @@ const AboutSettings: FC = () => {
onOpenWebsite(url)
}
const showLicense = () => {
window.api.openWebsite('https://raw.githubusercontent.com/kangfenmao/cherry-studio/main/LICENSE')
}
useEffect(() => {
runAsyncFunction(async () => {
const appInfo = await window.api.getAppInfo()
@@ -121,9 +125,7 @@ const AboutSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.website.title')}</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://easys.run/cherry-studio')}>
{t('settings.about.website.button')}
</Button>
<Button onClick={() => onOpenWebsite('https://cherry-ai.com')}>{t('settings.about.website.button')}</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
@@ -133,6 +135,11 @@ const AboutSettings: FC = () => {
</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.license.title')}</SettingRowTitle>
<Button onClick={showLicense}>{t('settings.about.license.button')}</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.contact.title')}</SettingRowTitle>
<Button onClick={mailto}>{t('settings.about.contact.button')}</Button>

View File

@@ -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

View File

@@ -6,9 +6,11 @@ import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { find } from 'lodash'
import { Model } from '@renderer/types'
import { useTranslation } from 'react-i18next'
import { EditOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
const ModelSettings: FC = () => {
const { defaultModel, topicNamingModel, setDefaultModel, setTopicNamingModel } = useDefaultModel()
const { defaultModel, topicNamingModel, translateModel, setDefaultModel, setTopicNamingModel, setTranslateModel } =
useDefaultModel()
const { providers } = useProviders()
const allModels = providers.map((p) => p.models).flat()
const { t } = useTranslation()
@@ -24,9 +26,16 @@ const ModelSettings: FC = () => {
}))
}))
const iconStyle = { fontSize: 16, marginRight: 8 }
return (
<SettingContainer>
<SettingTitle>{t('settings.models.default_assistant_model')}</SettingTitle>
<SettingTitle>
<div>
<MessageOutlined style={iconStyle} />
{t('settings.models.default_assistant_model')}
</div>
</SettingTitle>
<SettingDivider />
<Select
defaultValue={defaultModel.id}
@@ -35,7 +44,12 @@ const ModelSettings: FC = () => {
options={selectOptions}
/>
<div style={{ height: 30 }} />
<SettingTitle>{t('settings.models.topic_naming_model')}</SettingTitle>
<SettingTitle>
<div>
<EditOutlined style={iconStyle} />
{t('settings.models.topic_naming_model')}
</div>
</SettingTitle>
<SettingDivider />
<Select
defaultValue={topicNamingModel.id}
@@ -43,6 +57,21 @@ const ModelSettings: FC = () => {
onChange={(id) => setTopicNamingModel(find(allModels, { id }) as Model)}
options={selectOptions}
/>
<div style={{ height: 30 }} />
<SettingTitle>
<div>
<TranslationOutlined style={iconStyle} />
{t('settings.models.translate_model')}
</div>
</SettingTitle>
<SettingDivider />
<Select
defaultValue={translateModel?.id}
style={{ width: 200 }}
onChange={(id) => setTranslateModel(find(allModels, { id }) as Model)}
options={selectOptions}
placeholder={t('settings.models.empty')}
/>
</SettingContainer>
)
}

View File

@@ -42,7 +42,7 @@ const ProviderSettings: FC = () => {
apiKey: '',
apiHost: '',
models: [],
enabled: false,
enabled: true,
isSystem: false
} as Provider
addProvider(provider)
@@ -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`

View File

@@ -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);
}
`

View File

@@ -71,7 +71,8 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
provider: _provider.id,
group: getDefaultGroupName(model.id),
// @ts-ignore name
description: model?.description
description: model?.description,
owned_by: model?.owned_by
}))
)
setLoading(false)
@@ -178,7 +179,8 @@ const ListHeader = styled.div`
justify-content: space-between;
background-color: var(--color-background-soft);
padding: 8px 22px;
color: #ffffff50;
color: var(--color-text);
opacity: 0.4;
`
const ListItem = styled.div`
@@ -199,14 +201,14 @@ const ListItemHeader = styled.div`
`
const ListItemName = styled.div`
color: #fff;
color: var(--color-text);
font-size: 14px;
font-weight: 600;
margin-left: 6px;
`
const ModelHeaderTitle = styled.div`
color: #fff;
color: var(--color-text);
font-size: 18px;
font-weight: 600;
margin-right: 10px;

View File

@@ -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)`

View File

@@ -0,0 +1,306 @@
import {
CheckOutlined,
CopyOutlined,
SendOutlined,
SettingOutlined,
SwapOutlined,
WarningOutlined
} from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { fetchTranslate } from '@renderer/services/api'
import { getDefaultAssistant } from '@renderer/services/assistant'
import { Assistant, Message } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Button, Select, Space } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { isEmpty } from 'lodash'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import styled from 'styled-components'
let _text = ''
let _result = ''
let _targetLanguage = 'english'
const TranslatePage: FC = () => {
const { t } = useTranslation()
const [targetLanguage, setTargetLanguage] = useState(_targetLanguage)
const [text, setText] = useState(_text)
const [result, setResult] = useState(_result)
const { translateModel } = useDefaultModel()
const [loading, setLoading] = useState(false)
const [copied, setCopied] = useState(false)
_text = text
_result = result
_targetLanguage = targetLanguage
const languageOptions = [
{
value: 'english',
label: t('languages.english'),
emoji: '🇬🇧'
},
{
value: 'chinese',
label: t('languages.chinese'),
emoji: '🇨🇳'
},
{
value: 'chinese-traditional',
label: t('languages.chinese-traditional'),
emoji: '🇭🇰'
},
{
value: 'japanese',
label: t('languages.japanese'),
emoji: '🇯🇵'
},
{
value: 'korean',
label: t('languages.korean'),
emoji: '🇰🇷'
},
{
value: 'russian',
label: t('languages.russian'),
emoji: '🇷🇺'
},
{
value: 'spanish',
label: t('languages.spanish'),
emoji: '🇪🇸'
},
{
value: 'french',
label: t('languages.french'),
emoji: '🇫🇷'
},
{
value: 'italian',
label: t('languages.italian'),
emoji: '🇮🇹'
},
{
value: 'portuguese',
label: t('languages.portuguese'),
emoji: '🇵🇹'
},
{
value: 'arabic',
label: t('languages.arabic'),
emoji: '🇸🇦'
}
]
const onTranslate = async () => {
if (!text.trim()) {
return
}
if (!translateModel) {
window.message.error({
content: t('translate.error.not_configured'),
key: 'translate-message'
})
return
}
const assistant: Assistant = getDefaultAssistant()
assistant.model = translateModel
assistant.prompt = `Translate from input language to ${targetLanguage}, provide the translation result directly without any explanation, keep original format. If the target language is the same as the source language, do not translate. The text to be translated is as follows:\n\n ${text}`
const message: Message = {
id: uuid(),
role: 'user',
content: text,
assistantId: assistant.id,
topicId: uuid(),
modelId: translateModel.id,
createdAt: new Date().toISOString(),
status: 'sending'
}
setLoading(true)
const translateText = await fetchTranslate({ message, assistant })
setResult(translateText)
setLoading(false)
}
const onCopy = () => {
navigator.clipboard.writeText(result)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
useEffect(() => {
isEmpty(text) && setResult('')
}, [text])
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none' }}>{t('translate.title')}</NavbarCenter>
</Navbar>
<ContentContainer>
<MenuContainer>
<Select
showSearch
value="any"
style={{ width: 180 }}
optionFilterProp="label"
disabled
options={[{ label: t('translate.any.language'), value: 'any' }]}
/>
<SwapOutlined />
<Select
showSearch
value={targetLanguage}
style={{ width: 180 }}
optionFilterProp="label"
options={languageOptions}
onChange={(value) => setTargetLanguage(value)}
optionRender={(option) => (
<Space>
<span role="img" aria-label={option.data.label}>
{option.data.emoji}
</span>
{option.label}
</Space>
)}
/>
{translateModel && (
<Link to="/settings/model" style={{ color: 'var(--color-text-2)' }}>
<SettingOutlined />
</Link>
)}
{!translateModel && (
<Link to="/settings/model" style={{ marginLeft: -10 }}>
<Button
type="link"
style={{ color: 'var(--color-error)', textDecoration: 'underline' }}
icon={<WarningOutlined />}>
{t('translate.error.not_configured')}
</Button>
</Link>
)}
</MenuContainer>
<TranslateInputWrapper>
<InputContainer>
<Textarea
variant="borderless"
placeholder={t('translate.input.placeholder')}
value={text}
onChange={(e) => setText(e.target.value)}
disabled={loading}
allowClear
/>
<TranslateButton
type="primary"
loading={loading}
onClick={onTranslate}
disabled={!text.trim()}
icon={<SendOutlined />}>
{t('translate.button.translate')}
</TranslateButton>
</InputContainer>
<OutputContainer>
<OutputText>{result || t('translate.output.placeholder')}</OutputText>
<CopyButton
onClick={onCopy}
disabled={!result}
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyOutlined />}
/>
</OutputContainer>
</TranslateInputWrapper>
</ContentContainer>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
padding: 20px;
`
const MenuContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 15px;
gap: 20px;
`
const TranslateInputWrapper = styled.div`
display: flex;
flex-direction: row;
min-height: 350px;
gap: 20px;
`
const InputContainer = styled.div`
position: relative;
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
border: 1px solid var(--color-border);
border-radius: 10px;
`
const Textarea = styled(TextArea)`
display: flex;
flex: 1;
padding: 20px;
font-size: 16px;
overflow: auto;
.ant-input {
resize: none;
padding: 15px 20px;
}
`
const OutputContainer = styled.div`
position: relative;
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
padding: 10px;
background-color: var(--color-background-soft);
border-radius: 10px;
`
const OutputText = styled.div`
padding: 5px 10px;
max-height: calc(100vh - var(--navbar-height) - 120px);
overflow: auto;
white-space: pre-wrap;
`
const TranslateButton = styled(Button)`
position: absolute;
right: 15px;
bottom: 15px;
z-index: 10;
`
const CopyButton = styled(Button)`
position: absolute;
right: 15px;
bottom: 15px;
`
export default TranslatePage

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

@@ -1,4 +1,4 @@
import { Assistant, Message, Provider } from '@renderer/types'
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
import OpenAI from 'openai'
import Anthropic from '@anthropic-ai/sdk'
import { getDefaultModel, getTopNamingModel } from './assistant'
@@ -73,6 +73,33 @@ export default class ProviderSDK {
}
}
public async translate(message: Message, assistant: Assistant) {
const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel
const messages = [
{ role: 'system', content: assistant.prompt },
{ role: 'user', content: message.content }
]
if (this.isAnthropic) {
const response = await this.anthropicSdk.messages.create({
model: model.id,
messages: messages as MessageParam[],
max_tokens: 4096,
temperature: assistant?.settings?.temperature,
stream: false
})
return response.content[0].type === 'text' ? response.content[0].text : ''
} else {
const response = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: messages as ChatCompletionMessageParam[],
stream: false
})
return response.choices[0].message?.content || ''
}
}
public async summaries(messages: Message[], assistant: Assistant): Promise<string | null> {
const model = getTopNamingModel() || assistant.model || getDefaultModel()
@@ -107,6 +134,28 @@ export default class ProviderSDK {
}
}
public async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> {
const model = assistant.model
if (!model) {
return []
}
const response: any = await this.openaiSdk.request({
method: 'post',
path: '/advice_questions',
body: {
messages: messages.filter((m) => m.role === 'user').map((m) => ({ role: m.role, content: m.content })),
model: model.id,
max_tokens: 0,
temperature: 0,
n: 0
}
})
return response?.questions?.filter(Boolean)?.map((q: any) => ({ content: q })) || []
}
public async check(): Promise<{ valid: boolean; error: Error | null }> {
const model = this.provider.models[0]
const body = {

View File

@@ -1,12 +1,19 @@
import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Message, Provider, Topic } from '@renderer/types'
import { Assistant, Message, Provider, Suggestion, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import dayjs from 'dayjs'
import { getAssistantProvider, getDefaultModel, getProviderByModel, getTopNamingModel } from './assistant'
import {
getAssistantProvider,
getDefaultModel,
getProviderByModel,
getTopNamingModel,
getTranslateModel
} from './assistant'
import { EVENT_NAMES, EventEmitter } from './event'
import ProviderSDK from './ProviderSDK'
import { isEmpty } from 'lodash'
export async function fetchChatCompletion({
messages,
@@ -63,11 +70,33 @@ export async function fetchChatCompletion({
return message
}
export async function fetchTranslate({ message, assistant }: { message: Message; assistant: Assistant }) {
const model = getTranslateModel()
if (!model) {
return ''
}
const provider = getProviderByModel(model)
if (!hasApiKey(provider)) {
return ''
}
const providerSdk = new ProviderSDK(provider)
try {
return await providerSdk.translate(message, assistant)
} catch (error: any) {
return ''
}
}
export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) {
const model = getTopNamingModel() || assistant.model || getDefaultModel()
const provider = getProviderByModel(model)
if (!provider.apiKey) {
if (!hasApiKey(provider)) {
return null
}
@@ -80,6 +109,34 @@ export async function fetchMessagesSummary({ messages, assistant }: { messages:
}
}
export async function fetchSuggestions({
messages,
assistant
}: {
messages: Message[]
assistant: Assistant
}): Promise<Suggestion[]> {
console.debug('fetchSuggestions', messages, assistant)
const provider = getAssistantProvider(assistant)
const providerSdk = new ProviderSDK(provider)
console.debug('fetchSuggestions', provider)
const model = assistant.model
if (!model) {
return []
}
if (model.owned_by !== 'graphrag') {
return []
}
try {
return await providerSdk.suggestions(messages, assistant)
} catch (error: any) {
return []
}
}
export async function checkApi(provider: Provider) {
const model = provider.models[0]
const key = 'api-check'
@@ -114,6 +171,12 @@ export async function checkApi(provider: Provider) {
return valid
}
function hasApiKey(provider: Provider) {
if (!provider) return false
if (provider.id === 'ollama') return true
return !isEmpty(provider.apiKey)
}
export async function fetchModels(provider: Provider) {
const providerSdk = new ProviderSDK(provider)

View File

@@ -32,7 +32,11 @@ export function getTopNamingModel() {
return store.getState().llm.topicNamingModel
}
export function getAssistantProvider(assistant: Assistant) {
export function getTranslateModel() {
return store.getState().llm.translateModel
}
export function getAssistantProvider(assistant: Assistant): Provider {
const providers = store.getState().llm.providers
const provider = providers.find((p) => p.id === assistant.model?.provider)
return provider || getDefaultProvider()

View File

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

View File

@@ -7,11 +7,13 @@ export interface LlmState {
providers: Provider[]
defaultModel: Model
topicNamingModel: Model
translateModel: Model
}
const initialState: LlmState = {
defaultModel: SYSTEM_MODELS.openai[0],
topicNamingModel: SYSTEM_MODELS.openai[0],
translateModel: SYSTEM_MODELS.openai[0],
providers: [
{
id: 'openai',
@@ -174,6 +176,9 @@ const settingsSlice = createSlice({
},
setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => {
state.topicNamingModel = action.payload.model
},
setTranslateModel: (state, action: PayloadAction<{ model: Model }>) => {
state.translateModel = action.payload.model
}
}
})
@@ -186,7 +191,8 @@ export const {
addModel,
removeModel,
setDefaultModel,
setTopicNamingModel
setTopicNamingModel,
setTranslateModel
} = settingsSlice.actions
export default settingsSlice.reducer

View File

@@ -261,6 +261,15 @@ const migrateConfig = {
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 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

View File

@@ -55,6 +55,7 @@ export type Model = {
provider: string
name: string
group: string
owned_by?: string
description?: string
}
@@ -66,3 +67,7 @@ export type SystemAssistant = {
prompt: string
group: string
}
export type Suggestion = {
content: string
}

View File

@@ -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) {

View File

@@ -1,209 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="description"
content="Cherry Studio AI 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。" />
<meta name="keywords" content="Cherry Studio AI, AI 助手, GPT 客户端, 多模型, iOS, macOS, Windows, LLM" />
<meta name="author" content="kangfenmao" />
<link rel="canonical" href="https://easys.run/cherry-studio" />
<link rel="icon" type="image/png" href="https://easys.run/cherry-studio/logo.png" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://easys.run/cherry-studio" />
<meta property="og:title" content="Cherry Studio AI - 多模型 AI 助手" />
<meta
property="og:description"
content="Cherry Studio AI 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。" />
<meta property="og:image" content="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://x.com/kangfenmao" />
<meta property="twitter:title" content="Cherry Studio AI - 多模型 AI 助手" />
<meta
property="twitter:description"
content="Cherry Studio AI 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。" />
<meta
property="twitter:image"
content="https://github.com/kangfenmao/cherry-studio/blob/main/build/icon.png?raw=true" />
<title>Cherry Studio AI - 多模型AI助手</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
background-color: #000000;
color: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
text-align: center;
}
.logo {
width: 100px;
height: 100px;
margin-bottom: 20px;
border-radius: 10%;
margin-top: -10vh;
}
h1 {
font-size: 48px;
margin-bottom: 10px;
}
.description {
font-size: 18px;
margin-bottom: 30px;
color: #a0a0a0;
}
.download-buttons {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
margin-bottom: 30px;
}
.download-btn {
background-color: #ffffff;
color: #000000;
padding: 10px 20px;
border-radius: 25px;
text-decoration: none;
font-weight: bold;
transition: background-color 0.3s;
display: flex;
align-items: center;
justify-content: center;
}
.download-btn:hover {
background-color: #e0e0e0;
}
.download-btn svg {
margin-right: 10px;
width: 24px;
height: 24px;
}
.new-app {
margin-top: 20px;
font-size: 14px;
color: #a0a0a0;
}
.footer {
position: absolute;
bottom: 20px;
font-size: 14px;
color: #a0a0a0;
}
.footer a {
color: #a0a0a0;
text-decoration: none;
margin: 0 10px;
}
a {
color: #ffffff;
text-decoration: underline;
}
.loading {
flex-direction: row;
justify-content: center;
height: 200px;
}
[v-cloak] {
display: none;
}
</style>
</head>
<body id="app">
<a href="https://github.com/kangfenmao/cherry-studio" target="_blank">
<img src="https://easys.run/cherry-studio/logo.png" alt="Cherry Studio AI Logo" class="logo" />
</a>
<h1>Cherry Studio AI</h1>
<p class="description">Windows/macOS GPT 客户端</p>
<div class="download-buttons">
<a
:href="`https://github.com/kangfenmao/cherry-studio/releases/download/v${version}/Cherry-Studio-${version}-x64.dmg`"
class="download-btn">
<svg viewBox="0 0 384 512" width="24" height="24">
<path
fill="currentColor"
d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" />
</svg>
macOS Intel
</a>
<a
:href="`https://github.com/kangfenmao/cherry-studio/releases/download/v${version}/Cherry-Studio-${version}-arm64.dmg`"
class="download-btn">
<svg viewBox="0 0 384 512" width="24" height="24">
<path
fill="currentColor"
d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z" />
</svg>
macOS Apple Silicon
</a>
<a
:href="`https://github.com/kangfenmao/cherry-studio/releases/download/v${version}/Cherry-Studio-${version}-setup.exe`"
class="download-btn">
<svg viewBox="0 0 448 512" width="24" height="24">
<path
fill="currentColor"
d="M0 93.7l183.6-25.3v177.4H0V93.7zm0 324.6l183.6 25.3V268.4H0v149.9zm203.8 28L448 480V268.4H203.8v177.9zm0-380.6v180.1H448V32L203.8 65.7z" />
</svg>
下载 Windows 版本
</a>
</div>
<p class="new-app">
🎉 <a href="https://github.com/kangfenmao/cherry-studio" target="_blank">Cherry Studio AI</a> 最新版本
<a :href="`https://github.com/kangfenmao/cherry-studio/releases/tag/v${version}`" target="_blank" v-cloak
>v{{version}}</a
>
发布啦!
</p>
<div class="footer">
<a href="https://github.com/kangfenmao/cherry-studio" target="_blank">开源</a> |
<a href="https://github.com/kangfenmao/cherry-studio/blob/main/README.md" target="_blank">帮助</a> |
<a href="mailto:kangfenmao@qq.com" target="_blank">联系</a>
</div>
<!-- 结构化数据 -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Cherry Studio AI",
"applicationCategory": "UtilitiesApplication",
"operatingSystem": "iOS, macOS, Windows",
"description": "Cherry Studio AI 是一款强大的多模型 AI 助手,支持 iOS、macOS 和 Windows 平台。快速切换多个先进的 LLM 模型,提升工作学习效率。",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
}
}
</script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue
createApp({
data() {
return {
version: '0.3.2',
loading: true
}
},
mounted() {
this.loading = true
fetch('https://api.github.com/repos/kangfenmao/cherry-studio/releases/latest')
.then((response) => response.json())
.then((data) => (this.version = data.tag_name.replace('v', '')))
.finally(() => (this.loading = false))
}
}).mount('#app')
</script>
</body>
</html>

218
yarn.lock
View File

@@ -2710,6 +2710,20 @@ __metadata:
languageName: node
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":
version: 3.5.2
resolution: "ajv-keywords@npm:3.5.2"
@@ -2731,6 +2745,18 @@ __metadata:
languageName: node
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":
version: 5.0.1
resolution: "ansi-regex@npm:5.0.1"
@@ -3050,6 +3076,13 @@ __metadata:
languageName: node
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":
version: 1.0.7
resolution: "available-typed-arrays@npm:1.0.7"
@@ -3424,6 +3457,7 @@ __metadata:
electron-builder: "npm:^24.9.1"
electron-devtools-installer: "npm:^3.2.0"
electron-log: "npm:^5.1.5"
electron-store: "npm:^8.2.0"
electron-updater: "npm:^6.1.7"
electron-vite: "npm:^2.0.0"
electron-window-state: "npm:^5.0.3"
@@ -3445,6 +3479,7 @@ __metadata:
react-redux: "npm:^9.1.2"
react-router: "npm:6"
react-router-dom: "npm:6"
react-spinners: "npm:^0.14.1"
react-syntax-highlighter: "npm:^15.5.0"
redux-persist: "npm:^6.0.0"
sass: "npm:^1.77.2"
@@ -3632,6 +3667,24 @@ __metadata:
languageName: node
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":
version: 0.2.6
resolution: "config-file-ts@npm:0.2.6"
@@ -3766,6 +3819,15 @@ __metadata:
languageName: node
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":
version: 4.3.4
resolution: "debug@npm:4.3.4"
@@ -3961,6 +4023,15 @@ __metadata:
languageName: node
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":
version: 7.4.2
resolution: "dotenv-cli@npm:7.4.2"
@@ -4077,6 +4148,16 @@ __metadata:
languageName: node
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":
version: 1.4.776
resolution: "electron-to-chromium@npm:1.4.776"
@@ -4184,7 +4265,7 @@ __metadata:
languageName: node
linkType: hard
"env-paths@npm:^2.2.0":
"env-paths@npm:^2.2.0, env-paths@npm:^2.2.1":
version: 2.2.1
resolution: "env-paths@npm:2.2.1"
checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4
@@ -4809,6 +4890,13 @@ __metadata:
languageName: node
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":
version: 1.17.1
resolution: "fastq@npm:1.17.1"
@@ -4863,6 +4951,15 @@ __metadata:
languageName: node
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":
version: 5.0.0
resolution: "find-up@npm:5.0.0"
@@ -5883,6 +5980,13 @@ __metadata:
languageName: node
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":
version: 3.0.3
resolution: "is-path-inside@npm:3.0.3"
@@ -6135,6 +6239,20 @@ __metadata:
languageName: node
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":
version: 1.0.1
resolution: "json-stable-stringify-without-jsonify@npm:1.0.1"
@@ -6269,6 +6387,16 @@ __metadata:
languageName: node
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":
version: 6.0.0
resolution: "locate-path@npm:6.0.0"
@@ -6817,6 +6945,20 @@ __metadata:
languageName: node
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":
version: 1.0.1
resolution: "mimic-response@npm:1.0.1"
@@ -7214,6 +7356,15 @@ __metadata:
languageName: node
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":
version: 0.2.8
resolution: "openai-chat-tokens@npm:0.2.8"
@@ -7273,6 +7424,15 @@ __metadata:
languageName: node
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":
version: 3.1.0
resolution: "p-limit@npm:3.1.0"
@@ -7282,6 +7442,15 @@ __metadata:
languageName: node
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":
version: 5.0.0
resolution: "p-locate@npm:5.0.0"
@@ -7300,6 +7469,13 @@ __metadata:
languageName: node
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":
version: 1.0.0
resolution: "package-json-from-dist@npm:1.0.0"
@@ -7353,6 +7529,13 @@ __metadata:
languageName: node
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":
version: 4.0.0
resolution: "path-exists@npm:4.0.0"
@@ -7475,6 +7658,15 @@ __metadata:
languageName: node
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":
version: 3.1.0
resolution: "plist@npm:3.1.0"
@@ -8410,6 +8602,16 @@ __metadata:
languageName: node
linkType: hard
"react-spinners@npm:^0.14.1":
version: 0.14.1
resolution: "react-spinners@npm:0.14.1"
peerDependencies:
react: ^16.0.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0
checksum: 10c0/5b3c101f789716331a0b6afad156293fb9aa05620e65494753001afcdb611788057f379b5979b34d570d527fa978003293266b59db505bf2d243ebab899ceeda
languageName: node
linkType: hard
"react-syntax-highlighter@npm:^15.5.0":
version: 15.5.0
resolution: "react-syntax-highlighter@npm:15.5.0"
@@ -8583,6 +8785,13 @@ __metadata:
languageName: node
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":
version: 7.3.0
resolution: "require-in-the-middle@npm:7.3.0"
@@ -9491,6 +9700,13 @@ __metadata:
languageName: node
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":
version: 1.0.2
resolution: "typed-array-buffer@npm:1.0.2"