Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29d6d607da | ||
|
|
e64375a74c | ||
|
|
4689bb53e9 | ||
|
|
e00c66e54a | ||
|
|
62b0908dfa | ||
|
|
cb0b9de1e9 | ||
|
|
d8d4afbc0d | ||
|
|
c50ff4585a | ||
|
|
a5ee8548f3 | ||
|
|
15b286a095 | ||
|
|
d47d4a158d | ||
|
|
cd85dcddf8 | ||
|
|
925a9fb8ec | ||
|
|
17c3437e02 | ||
|
|
69293846fc | ||
|
|
20a7fbfc48 | ||
|
|
64d4b8450a | ||
|
|
f080fc5048 | ||
|
|
50f08124d7 | ||
|
|
b91081ef99 | ||
|
|
d869ec9a9b | ||
|
|
70c4354d6c | ||
|
|
527c4e77dc | ||
|
|
2483ce3bb4 | ||
|
|
db3f8b8bee | ||
|
|
45bf3d4e86 | ||
|
|
59b39dc41a | ||
|
|
a267a8d4c3 | ||
|
|
5b123f2c33 | ||
|
|
fe34fb3c25 | ||
|
|
e6359d2048 |
114
LICENSE
114
LICENSE
@@ -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:**
|
||||
|
||||
@@ -56,8 +56,5 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
1. 保存聊天页面状态,切换页面后恢复
|
||||
2. 修复默认助手名称为空时候的显示问题
|
||||
3. 简化系统内置智能体提示词长度
|
||||
4. 增加单个聊天内容保存到本地功能
|
||||
5. 系统内置提供商支持修改 API 地址
|
||||
新增发送按钮
|
||||
输入区域展开可以全屏显示
|
||||
|
||||
@@ -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
15
src/main/config.ts
Normal 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'
|
||||
}
|
||||
@@ -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('触发检查更新')
|
||||
|
||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@@ -12,6 +12,7 @@ declare global {
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
saveFile: (path: string, content: string) => void
|
||||
setTheme: (theme: 'light' | 'dark') => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ const api = {
|
||||
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
||||
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
|
||||
saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content)
|
||||
saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme)
|
||||
}
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<title>Cherry Studio</title>
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' *; 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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
55
src/renderer/src/assets/fonts/icon-fonts/iconfont.css
Normal file
55
src/renderer/src/assets/fonts/icon-fonts/iconfont.css
Normal 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";
|
||||
}
|
||||
|
||||
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.ttf
Normal file
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff
Normal file
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2
Normal file
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.9 KiB |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '阿拉伯文'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import localforage from 'localforage'
|
||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import * as Sentry from '@sentry/electron/renderer'
|
||||
import { isProduction, loadScript } from './utils'
|
||||
import { ThemeMode } from './store/settings'
|
||||
|
||||
async function initSentry() {
|
||||
if (await isProduction()) {
|
||||
@@ -21,12 +22,12 @@ async function initSentry() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function initMermaid() {
|
||||
export async function initMermaid(theme: ThemeMode) {
|
||||
if (!window.mermaid) {
|
||||
await loadScript('https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js')
|
||||
window.mermaid.initialize({
|
||||
startOnLoad: true,
|
||||
theme: 'dark',
|
||||
theme: theme === ThemeMode.dark ? 'dark' : 'default',
|
||||
securityLevel: 'loose'
|
||||
})
|
||||
window.mermaid.contentLoaded()
|
||||
|
||||
@@ -116,12 +116,16 @@ const AssistantCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 16px;
|
||||
background-color: #111;
|
||||
border: 0.5px solid #151515;
|
||||
background-color: var(--color-background-soft);
|
||||
border: 0.5px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
const EmojiHeader = styled.div`
|
||||
width: 25px;
|
||||
@@ -148,7 +152,7 @@ const AssistantName = styled(Title)`
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
color: var(--color-white);
|
||||
font-weight: 900;
|
||||
`
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore
|
||||
import Navigation from './components/NavigationCenter'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { Switch } from 'antd'
|
||||
|
||||
let _activeAssistant: Assistant
|
||||
|
||||
@@ -18,6 +20,7 @@ const HomePage: FC = () => {
|
||||
const { rightSidebarShown, toggleRightSidebar } = useShowRightSidebar()
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -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
|
||||
119
src/renderer/src/pages/home/components/Suggestions.tsx
Normal file
119
src/renderer/src/pages/home/components/Suggestions.tsx
Normal 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
|
||||
@@ -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;
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)`
|
||||
|
||||
306
src/renderer/src/pages/translate/TranslatePage.tsx
Normal file
306
src/renderer/src/pages/translate/TranslatePage.tsx
Normal 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
|
||||
37
src/renderer/src/providers/AntdProvider.tsx
Normal file
37
src/renderer/src/providers/AntdProvider.tsx
Normal 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
|
||||
43
src/renderer/src/providers/ThemeProvider.tsx
Normal file
43
src/renderer/src/providers/ThemeProvider.tsx
Normal 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)
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -19,7 +19,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 16,
|
||||
version: 17,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -261,6 +261,15 @@ const migrateConfig = {
|
||||
showInputEstimatedTokens: false
|
||||
}
|
||||
}
|
||||
},
|
||||
'17': (state: RootState) => {
|
||||
return {
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
theme: 'auto'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export type SendMessageShortcut = 'Enter' | 'Shift+Enter'
|
||||
|
||||
export enum ThemeMode {
|
||||
light = 'light',
|
||||
dark = 'dark',
|
||||
auto = 'auto'
|
||||
}
|
||||
|
||||
export interface SettingsState {
|
||||
showRightSidebar: boolean
|
||||
showAssistants: boolean
|
||||
@@ -12,6 +18,7 @@ export interface SettingsState {
|
||||
showMessageDivider: boolean
|
||||
messageFont: 'system' | 'serif'
|
||||
showInputEstimatedTokens: boolean
|
||||
theme: ThemeMode
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
@@ -23,7 +30,8 @@ const initialState: SettingsState = {
|
||||
userName: '',
|
||||
showMessageDivider: true,
|
||||
messageFont: 'system',
|
||||
showInputEstimatedTokens: false
|
||||
showInputEstimatedTokens: false,
|
||||
theme: ThemeMode.light
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
@@ -59,6 +67,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setShowInputEstimatedTokens: (state, action: PayloadAction<boolean>) => {
|
||||
state.showInputEstimatedTokens = action.payload
|
||||
},
|
||||
setTheme: (state, action: PayloadAction<ThemeMode>) => {
|
||||
state.theme = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -73,7 +84,8 @@ export const {
|
||||
setUserName,
|
||||
setShowMessageDivider,
|
||||
setMessageFont,
|
||||
setShowInputEstimatedTokens
|
||||
setShowInputEstimatedTokens,
|
||||
setTheme
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
218
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user