Compare commits

...

49 Commits

Author SHA1 Message Date
kangfenmao
297539bab7 chore(version): 0.4.7 2024-08-02 11:32:29 +08:00
kangfenmao
911c2d0202 fix: footnote style 2024-08-02 11:30:06 +08:00
kangfenmao
2969a05f10 feat: enhance markdown style 2024-08-02 10:39:13 +08:00
kangfenmao
5d90489a04 style: change import order 2024-08-02 10:11:18 +08:00
kangfenmao
18fa1c92a4 feat(provider): sillicon api key use referrer link 2024-08-02 09:24:31 +08:00
kangfenmao
937e62bf9d feat(provider): add gpt-4o-mini model 2024-08-02 09:24:00 +08:00
kangfenmao
6291a463d8 perf(messages): usememo & usecallback message component 2024-08-01 23:55:51 +08:00
kangfenmao
681c93f5eb chore(version): 0.4.6 2024-08-01 15:36:07 +08:00
kangfenmao
23687f119d fix(SendMessageButton.tsx): remove unnecessary placement prop from SendMessageButton to prevent potential UI alignment issues 2024-08-01 15:23:12 +08:00
kangfenmao
0bcdffc159 fix(SettingsTab.tsx): correct the temperature label 2024-08-01 15:19:45 +08:00
kangfenmao
b04b0cc8a6 feat: add markdown footnote 2024-08-01 15:18:09 +08:00
kangfenmao
c9a964d8f8 feat: add markdown plugins remark-gfm remark-math rehype-katex 2024-08-01 14:51:20 +08:00
kangfenmao
86fc4676ba feat: add link component 2024-08-01 14:28:18 +08:00
kangfenmao
527afa1357 chore(version): 0.4.5 2024-08-01 00:05:16 +08:00
kangfenmao
384178c617 style(Message.tsx): increase padding in MessageContainer 2024-08-01 00:04:47 +08:00
kangfenmao
c53e35db76 feat: use poppins fonts 2024-07-31 23:20:28 +08:00
kangfenmao
c36075f0b5 fix: optimize interface display style 2024-07-31 21:04:09 +08:00
kangfenmao
5c95373a37 feat: new window style 2024-07-31 17:30:17 +08:00
kangfenmao
29d6d607da chore(version): 0.4.4 2024-07-31 13:54:04 +08:00
kangfenmao
e64375a74c feat(Inputbar.tsx): change height to min-height for Inputbar 2024-07-31 13:41:02 +08:00
kangfenmao
4689bb53e9 chore(package.json): add publish script to automate the release and patch version push process 2024-07-31 13:11:31 +08:00
kangfenmao
e00c66e54a chore(version): 0.4.3 2024-07-31 13:08:19 +08:00
kangfenmao
62b0908dfa feat: add send message button 2024-07-31 13:07:02 +08:00
kangfenmao
cb0b9de1e9 feat: default enable new added provider 2024-07-31 12:21:46 +08:00
kangfenmao
d8d4afbc0d feat: add message suggestions 2024-07-31 12:13:03 +08:00
kangfenmao
c50ff4585a chore(version): 0.4.2 2024-07-30 17:53:45 +08:00
kangfenmao
a5ee8548f3 feat(AboutSettings): implement functionality to open license page from about settings 2024-07-30 16:33:58 +08:00
kangfenmao
15b286a095 doc: update LICENSE 2024-07-30 16:13:32 +08:00
kangfenmao
d47d4a158d docs: change offical website url 2024-07-30 15:31:17 +08:00
kangfenmao
cd85dcddf8 remove: website 2024-07-30 15:30:35 +08:00
kangfenmao
925a9fb8ec fix: delete provider crash 2024-07-30 15:30:09 +08:00
kangfenmao
17c3437e02 chore(version): 0.4.1 2024-07-29 18:18:03 +08:00
kangfenmao
69293846fc fix: model list text color 2024-07-29 18:17:50 +08:00
kangfenmao
20a7fbfc48 fix(ProviderSDK.ts): translation message 2024-07-29 17:45:08 +08:00
kangfenmao
64d4b8450a style(website): adjust border-radius of images to 20% 2024-07-29 17:36:27 +08:00
kangfenmao
f080fc5048 chore(version): 0.4.0 2024-07-29 17:33:09 +08:00
kangfenmao
50f08124d7 feat: add dark and light theme 2024-07-29 17:14:49 +08:00
kangfenmao
b91081ef99 docs(index.html): update website URLs from easys.run to cherry-ai.com 2024-07-29 09:55:24 +08:00
kangfenmao
d869ec9a9b chore(version): 0.3.9 2024-07-29 09:16:46 +08:00
kangfenmao
70c4354d6c feat: add model logo on select model dropdown 2024-07-28 15:10:36 +08:00
kangfenmao
527c4e77dc fix(Message.tsx): add optional chaining to assistant.name to prevent potential undefined errors 2024-07-28 11:16:16 +08:00
kangfenmao
2483ce3bb4 chore(version): 0.3.8 2024-07-28 02:28:48 +08:00
kangfenmao
db3f8b8bee refactor(TranslatePage.tsx): simplify OutputText styling for cleaner code structure 2024-07-28 02:28:48 +08:00
kangfenmao
45bf3d4e86 fix(index.html): update Content-Security-Policy to allow fonts 2024-07-28 01:37:43 +08:00
kangfenmao
59b39dc41a feat(TranslatePage.tsx): add markdown style to handle whitespace properly in translation output 2024-07-28 01:32:03 +08:00
kangfenmao
a267a8d4c3 feat: add translation module 2024-07-28 01:07:15 +08:00
kangfenmao
5b123f2c33 fix(markdown.scss): replace :first-of-type with :first-child for consistent styling of first elements 2024-07-26 18:02:50 +08:00
kangfenmao
fe34fb3c25 fix(api.ts): modify provider apiKey check to exclude 'ollama' provider 2024-07-26 18:02:32 +08:00
kangfenmao
e6359d2048 feat(markdown.scss): add white-space: pre-wrap to code elements 2024-07-26 17:22:48 +08:00
92 changed files with 2833 additions and 1006 deletions

View File

@@ -1,5 +1,5 @@
module.exports = {
plugins: ['unused-imports'],
plugins: ['unused-imports', 'simple-import-sort'],
extends: [
'eslint:recommended',
'plugin:react/recommended',
@@ -14,12 +14,7 @@ module.exports = {
'unused-imports/no-unused-imports': 'error',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'react/prop-types': 'off',
'sort-imports': [
'error',
{
ignoreCase: true,
ignoreDeclarationSort: true
}
]
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error'
}
}

View File

@@ -60,7 +60,7 @@ jobs:
- name: Release
uses: softprops/action-gh-release@v2
with:
draft: false
draft: true
files: |
dist/*.exe
dist/*.zip

114
LICENSE
View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import react from '@vitejs/plugin-react'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { resolve } from 'path'
export default defineConfig({

View File

@@ -1,6 +1,6 @@
{
"name": "cherry-studio",
"version": "0.3.7",
"version": "0.4.7",
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
"author": "kangfenmao@qq.com",
@@ -19,15 +19,18 @@
"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"
"electron-window-state": "^5.0.3",
"eslint-plugin-simple-import-sort": "^12.1.1"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.24.3",
@@ -69,8 +72,12 @@
"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",
"rehype-katex": "^7.0.0",
"remark-gfm": "^4.0.0",
"remark-math": "^6.0.0",
"sass": "^1.77.2",
"styled-components": "^6.1.11",
"typescript": "^5.3.3",

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

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

View File

@@ -1,6 +1,6 @@
import { dialog, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
import { writeFile } from 'fs'
import logger from 'electron-log'
import { writeFile } from 'fs'
export async function saveFile(_: Electron.IpcMainInvokeEvent, fileName: string, content: string): Promise<void> {
try {

View File

@@ -4,9 +4,11 @@ import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, shell } from 'ele
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 +17,8 @@ function createWindow() {
defaultHeight: 670
})
const theme = appConfig.get('theme') || 'light'
// Create the browser window.
const mainWindow = new BrowserWindow({
x: mainWindowState.x,
@@ -25,12 +29,10 @@ function createWindow() {
minHeight: 500,
show: true,
autoHideMenuBar: true,
transparent: process.platform === 'darwin',
vibrancy: 'fullscreen-ui',
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 +120,12 @@ app.whenReady().then(() => {
ipcMain.handle('save-file', saveFile)
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
appConfig.set('theme', theme)
mainWindow?.setTitleBarOverlay &&
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
})
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.handle('check-for-update', async () => {
autoUpdater.logger?.info('触发检查更新')

View File

@@ -1,6 +1,6 @@
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
import logger from 'electron-log'
import { BrowserWindow, dialog } from 'electron'
import logger from 'electron-log'
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
export default class AppUpdater {
autoUpdater: _AppUpdater = autoUpdater

View File

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

View File

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

View File

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

View File

@@ -1,33 +1,38 @@
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>
)
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,47 @@
@font-face {
font-family: 'Poppins';
src: url(Poppins-Thin.ttf) format('truetype');
font-weight: 100;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-ExtraLight.ttf) format('truetype');
font-weight: 200;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-Light.ttf) format('truetype');
font-weight: 300;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-Regular.ttf) format('truetype');
font-weight: 400;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-Medium.ttf) format('truetype');
font-weight: 500;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-SemiBold.ttf) format('truetype');
font-weight: 600;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-Bold.ttf) format('truetype');
font-weight: 700;
}
@font-face {
font-family: 'Poppins';
src: url(Poppins-ExtraBold.ttf) format('truetype');
font-weight: 800;
}

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,11 +1,7 @@
@import 'https://at.alicdn.com/t/c/font_4563475_hrx8c92awui.css';
@import './markdown.scss';
@import './scrollbar.scss';
// @font-face {
// font-family: 'Playwrite';
// src: url(../fonts/Playwrite.ttf) format('truetype');
// }
@import '../fonts/icon-fonts/iconfont.css';
@import '../fonts/Poppins/Poppins.css';
:root {
--color-white: #ffffff;
@@ -28,24 +24,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: rgba(0, 0, 0, 0.8);
--sidebar-background: rgba(0, 0, 0, 0.8);
--navbar-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: 115px;
}
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: rgba(255, 255, 255, 0.8);
--sidebar-background: rgba(255, 255, 255, 0.8);
}
*,
@@ -64,12 +103,21 @@ body {
display: flex;
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
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;
background: transparent !important;
font-family:
-apple-system,
BlinkMacSystemFont,
'Microsoft YaHei',
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue' sans-serif;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@@ -97,3 +145,21 @@ body,
#inputbar .ant-input {
resize: none;
}
.chat-nav-dropdown {
.ant-dropdown-menu {
padding-bottom: 12px;
}
}
.loader {
width: 16px;
height: 16px;
border-radius: 50%;
background-color: #000;
box-shadow:
32px 0 #000,
-32px 0 #000;
position: relative;
animation: flash 0.5s ease-out infinite alternate;
}

View File

@@ -1,5 +1,5 @@
.markdown {
color: #f1f1f1;
color: var(--color-text);
font-size: 15px;
line-height: 1.6;
user-select: text;
@@ -8,16 +8,16 @@
margin-bottom: 5px;
}
p:first-of-type {
p:first-child {
margin-top: 0;
}
h1:first-of-type,
h2:first-of-type,
h3:first-of-type,
h4:first-of-type,
h5:first-of-type,
h6:first-of-type {
h1:first-child,
h2:first-child,
h3:first-child,
h4:first-child,
h5:first-child,
h6:first-child {
margin-top: 0;
}
@@ -33,58 +33,198 @@
h1 {
font-size: 2em;
color: #fff;
border-bottom: 0.5px solid var(--color-border);
padding-bottom: 0.3em;
}
h2 {
font-size: 1.5em;
color: #fff;
border-bottom: 0.5px solid var(--color-border);
padding-bottom: 0.3em;
}
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 {
margin-bottom: 0.5em;
}
li > ul,
li > ol {
margin: 0.5em 0;
}
hr {
border: none;
border-top: 1px solid #555;
border-top: 0.5px solid var(--color-border);
margin: 20px 0;
background-color: #555;
background-color: var(--color-border);
}
span {
word-break: break-all;
}
code {
white-space: pre-wrap;
font-family: 'Courier New', Courier, monospace;
}
p code {
background: var(--color-background-mute);
padding: 3px 5px;
border-radius: 5px;
}
pre {
white-space: pre-wrap;
padding: 1em;
border-radius: 5px;
overflow-x: auto;
pre {
margin: 0 !important;
}
code {
background: none;
padding: 0;
border-radius: 0;
}
}
blockquote {
margin: 1em 0;
padding-left: 1em;
color: var(--color-text-light);
border-left: 4px solid var(--color-border);
}
table {
border-collapse: collapse;
margin: 1em 0;
width: 100%;
}
th,
td {
border: 0.5px solid var(--color-border);
padding: 0.5em;
}
th {
background-color: var(--color-background-mute);
font-weight: bold;
}
img {
max-width: 100%;
height: auto;
}
a {
color: var(--color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
strong {
font-weight: bold;
}
em {
font-style: italic;
}
del {
text-decoration: line-through;
}
sup,
sub {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
.footnote-ref {
font-size: 0.8em;
vertical-align: super;
line-height: 0;
margin: 0 2px;
color: var(--color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.footnotes {
margin-top: 1em;
padding-top: 1em;
border-top: 1px solid var(--color-border);
ol {
padding-left: 1em;
}
li {
font-size: 0.9em;
margin-bottom: 0.5em;
color: var(--color-text-light);
p {
display: inline;
margin: 0;
}
}
.footnote-backref {
font-size: 0.8em;
vertical-align: super;
line-height: 0;
margin-left: 5px;
color: var(--color-primary);
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
}
}

View File

@@ -1,6 +1,7 @@
/* 全局初始化滚动条样式 */
::-webkit-scrollbar {
width: 3px;
height: 3px;
}
::-webkit-scrollbar-track {
@@ -8,8 +9,8 @@
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
background: var(--color-scrollbar-thumb);
&:hover {
background: rgba(255, 255, 255, 0.3);
background: var(--color-scrollbar-thumb-hover);
}
}

View File

@@ -1,11 +1,12 @@
import { Input, Modal } from 'antd'
import { useState } from 'react'
import { TopView } from '../TopView'
import { Box } from '../Layout'
import { Assistant } from '@renderer/types'
import { Input, Modal } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Box } from '../Layout'
import { TopView } from '../TopView'
interface AssistantSettingPopupShowParams {
assistant: Assistant
}

View File

@@ -1,7 +1,8 @@
import { Input, InputProps, Modal } from 'antd'
import { useState } from 'react'
import { TopView } from '../TopView'
import { Box } from '../Layout'
import { TopView } from '../TopView'
interface PromptPopupShowParams {
title: string

View File

@@ -1,7 +1,8 @@
import { Modal } from 'antd'
import { useState } from 'react'
import { TopView } from '../TopView'
import { Box } from '../Layout'
import { TopView } from '../TopView'
interface ShowParams {
title: string

View File

@@ -1,3 +1,4 @@
import { isMac } from '@renderer/config/constant'
import { FC, PropsWithChildren } from 'react'
import styled from 'styled-components'
@@ -26,14 +27,14 @@ const NavbarContainer = styled.div`
min-height: var(--navbar-height);
max-height: var(--navbar-height);
-webkit-app-region: drag;
background-color: var(--navbar-background);
margin-left: calc(var(--sidebar-width) * -1);
padding-left: var(--sidebar-width);
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
border-bottom: 0.5px solid var(--color-border);
background-color: var(--navbar-background);
`
const NavbarLeftContainer = styled.div`
min-width: var(--assistants-width);
min-width: ${isMac ? 'var(--assistants-width)' : 'calc(var(--sidebar-width) + var(--assistants-width))'};
padding: 0 10px;
display: flex;
flex-direction: row;
@@ -47,7 +48,7 @@ const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
padding: 0 20px;
padding: 0 ${isMac ? '20px' : '15px'};
font-size: 14px;
font-weight: bold;
color: var(--color-text-1);

View File

@@ -1,9 +1,10 @@
import { FC } from 'react'
import { TranslationOutlined } from '@ant-design/icons'
import Logo from '@renderer/assets/images/logo.png'
import styled from 'styled-components'
import { Link, useLocation } from 'react-router-dom'
import { isWindows } from '@renderer/config/constant'
import useAvatar from '@renderer/hooks/useAvatar'
import { isMac, isWindows } from '@renderer/config/constant'
import { FC } from 'react'
import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components'
const Sidebar: FC = () => {
const { pathname } = useLocation()
@@ -13,7 +14,6 @@ const Sidebar: FC = () => {
return (
<Container style={isWindows ? { paddingTop: 0 } : {}}>
{isMac ? <PlaceholderBorderMac /> : <PlaceholderBorderWin />}
<StyledLink to="/">
<AvatarImg src={avatar || Logo} draggable={false} />
</StyledLink>
@@ -29,6 +29,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>
@@ -47,13 +52,13 @@ const Container = styled.div`
flex-direction: column;
align-items: center;
padding: 8px 0;
min-width: var(--sidebar-width);
min-height: 100%;
width: var(--sidebar-width);
height: calc(100vh - var(--navbar-height));
-webkit-app-region: drag !important;
background-color: #1f1f1f;
border-right: 0.5px solid var(--color-border);
padding-top: var(--navbar-height);
position: relative;
margin-top: var(--navbar-height);
margin-bottom: var(--navbar-height);
background-color: var(--sidebar-background);
`
const AvatarImg = styled.img`
@@ -62,7 +67,7 @@ const AvatarImg = styled.img`
height: 28px;
background-color: var(--color-background-soft);
margin: 5px 0;
margin-top: ${isMac ? '16px' : '7px'};
margin-top: 5px;
`
const MainMenus = styled.div`
display: flex;
@@ -85,22 +90,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);
}
}
@@ -114,24 +125,4 @@ const StyledLink = styled(Link)`
}
`
const PlaceholderBorderMac = styled.div`
width: var(--sidebar-width);
height: var(--navbar-height);
background: var(--navbar-background);
border-right: 1px solid var(--navbar-background);
border-bottom: 0.5px solid var(--color-border);
position: absolute;
top: 0;
left: 0;
`
const PlaceholderBorderWin = styled.div`
width: var(--sidebar-width);
height: var(--navbar-height);
position: absolute;
border-right: 1px solid var(--navbar-background);
top: -1px;
right: -1px;
`
export default Sidebar

View File

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

View File

@@ -10,7 +10,7 @@
{
"id": 2,
"name": "🎯 策略产品经理 - Strategy Product Manager",
"emoji": "🎯 ",
"emoji": "🎯",
"group": "职业",
"prompt": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。",
"description": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。"

View File

@@ -5,10 +5,17 @@ type SystemModel = Model & { enabled: boolean }
export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
openai: [
{
id: 'gpt-3.5-turbo',
id: 'gpt-4o',
provider: 'openai',
name: 'GPT-3.5 Turbo',
group: 'GPT 3.5',
name: ' GPT-4o',
group: 'GPT 4o',
enabled: true
},
{
id: 'gpt-4o-mini',
provider: 'openai',
name: ' GPT-4o-mini',
group: 'GPT 4o',
enabled: true
},
{
@@ -24,13 +31,6 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
name: ' GPT-4',
group: 'GPT 4',
enabled: true
},
{
id: 'gpt-4o',
provider: 'openai',
name: ' GPT-4o',
group: 'GPT 4o',
enabled: true
}
],
silicon: [

View File

@@ -1,28 +1,28 @@
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.jpeg'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg'
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg'
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
import YiModelLogo from '@renderer/assets/images/models/yi.svg'
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png'
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg'
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpeg'
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.jpeg'
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
export function getProviderLogo(providerId: string) {
switch (providerId) {
@@ -58,6 +58,10 @@ export function getProviderLogo(providerId: string) {
}
export function getModelLogo(modelId: string) {
if (!modelId) {
return undefined
}
const logoMap = {
gpt: ChatGPTModelLogo,
glm: ChatGLMModelLogo,
@@ -103,7 +107,7 @@ export const PROVIDER_CONFIG = {
},
websites: {
official: 'https://www.siliconflow.cn/',
apiKey: 'https://cloud.siliconflow.cn/account/ak',
apiKey: 'https://cloud.siliconflow.cn/account/ak?referrer=clxty1xuy0014lvqwh5z50i88',
docs: 'https://docs.siliconflow.cn/',
models: 'https://docs.siliconflow.cn/docs/model-names'
}

View File

@@ -1,8 +1,8 @@
/// <reference types="vite/client" />
import type KeyvStorage from '@kangfenmao/keyv-storage'
import { MessageInstance } from 'antd/es/message/interface'
import { HookAPI } from 'antd/es/modal/useModal'
import type KeyvStorage from '@kangfenmao/keyv-storage'
declare global {
interface Window {

View File

@@ -1,21 +1,22 @@
import { i18nInit } from '@renderer/i18n'
import i18n from '@renderer/i18n'
import LocalStorage from '@renderer/services/storage'
import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
import { runAsyncFunction } from '@renderer/utils'
import { useEffect } from 'react'
import { useSettings } from './useSettings'
export function useAppInit() {
const dispatch = useAppDispatch()
const { proxyUrl } = useSettings()
const { language } = useSettings()
useEffect(() => {
runAsyncFunction(async () => {
const storedImage = await LocalStorage.getImage('avatar')
storedImage && dispatch(setAvatar(storedImage))
})
i18nInit()
}, [dispatch])
useEffect(() => {
@@ -28,4 +29,8 @@ export function useAppInit() {
useEffect(() => {
proxyUrl && window.api.setProxy(proxyUrl)
}, [proxyUrl])
useEffect(() => {
i18n.changeLanguage(language || navigator.language || 'en-US')
}, [language])
}

View File

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

View File

@@ -1,3 +1,4 @@
import { createSelector } from '@reduxjs/toolkit'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addModel,
@@ -8,8 +9,8 @@ import {
updateProviders
} from '@renderer/store/llm'
import { Assistant, Model, Provider } from '@renderer/types'
import { useDefaultModel } from './useAssistant'
import { createSelector } from '@reduxjs/toolkit'
const selectEnabledProviders = createSelector(
(state) => state.llm.providers,
@@ -17,7 +18,7 @@ const selectEnabledProviders = createSelector(
)
export function useProviders() {
const providers = useAppSelector(selectEnabledProviders)
const providers: Provider[] = useAppSelector(selectEnabledProviders)
const dispatch = useAppDispatch()
return {
@@ -47,7 +48,7 @@ export function useProvider(id: string) {
return {
provider,
models: provider.models,
models: provider?.models || [],
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
addModel: (model: Model) => dispatch(addModel({ providerId: id, model })),
removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model }))

View File

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

View File

@@ -2,12 +2,12 @@ import { Assistant, Topic } from '@renderer/types'
import { find } from 'lodash'
import { useEffect, useState } from 'react'
let _activeTopic: Topic
const activeTopicsMap = new Map<string, Topic>()
export function useActiveTopic(assistant: Assistant) {
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
const [activeTopic, setActiveTopic] = useState(activeTopicsMap.get(assistant.id) || assistant?.topics[0])
_activeTopic = activeTopic
activeTopicsMap.set(assistant.id, activeTopic)
useEffect(() => {
// activeTopic not in assistant.topics

View File

@@ -1,4 +1,3 @@
import store from '@renderer/store'
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
@@ -25,7 +24,8 @@ const resources = {
regenerate: 'Regenerate',
provider: 'Provider',
you: 'You',
save: 'Save'
save: 'Save',
footnotes: 'References'
},
button: {
add: 'Add',
@@ -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'
}
}
},
@@ -180,7 +209,8 @@ const resources = {
copy: '复制',
regenerate: '重新生成',
provider: '提供商',
you: '用户'
you: '用户',
footnote: '引用内容'
},
button: {
add: '添加',
@@ -189,7 +219,7 @@ const resources = {
select_model: '选择模型'
},
message: {
copied: '已复制!',
copied: '已复制',
'assistant.added.content': '智能体添加成功',
'message.delete.title': '删除消息',
'message.delete.content': '确定要删除此消息吗?',
@@ -210,8 +240,8 @@ const resources = {
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
'default.topic.name': '默认话题',
'topics.title': '话题',
'topics.auto_rename': 'AI 重命名',
'topics.edit.title': '重命名',
'topics.auto_rename': '生成话题名',
'topics.edit.title': '编辑话题名',
'topics.edit.placeholder': '输入新名称',
'topics.delete.all.title': '删除所有话题',
'topics.delete.all.content': '确定要删除所有话题吗?',
@@ -234,7 +264,8 @@ const resources = {
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10代码生成建议 5-10',
'settings.reset': '重置',
'settings.set_as_default': '应用到默认助手',
'settings.max': '不限'
'settings.max': '不限',
'suggestions.title': '建议的问题'
},
apps: {
title: '智能体'
@@ -258,7 +289,7 @@ const resources = {
title: '设置',
general: '常规设置',
provider: '模型提供商',
model: '模型设置',
model: '默认模型',
assistant: '默认助手',
about: '关于我们',
'messages.model.title': '模型设置',
@@ -280,6 +311,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 +342,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: '阿拉伯文'
}
}
}
@@ -326,8 +385,4 @@ i18n.use(initReactI18next).init({
}
})
export function i18nInit() {
i18n.changeLanguage(store.getState().settings.language || 'en-US')
}
export default i18n

View File

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

View File

@@ -1,10 +1,12 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './assets/styles/index.scss'
import './init'
import './i18n'
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />

View File

@@ -1,13 +1,13 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import SYSTEM_ASSISTANTS from '@renderer/config/assistants.json'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { getDefaultAssistant } from '@renderer/services/assistant'
import { SystemAssistant } from '@renderer/types'
import { Col, Row, Typography } from 'antd'
import { find, groupBy } from 'lodash'
import { FC } from 'react'
import styled from 'styled-components'
import { SystemAssistant } from '@renderer/types'
import { getDefaultAssistant } from '@renderer/services/assistant'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useTranslation } from 'react-i18next'
import SYSTEM_ASSISTANTS from '@renderer/config/assistants.json'
import styled from 'styled-components'
const { Title } = Typography
@@ -101,6 +101,7 @@ const ContentContainer = styled.div`
justify-content: center;
height: 100%;
overflow-y: scroll;
background-color: var(--color-background);
`
const AssistantsContainer = styled.div`
@@ -116,12 +117,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 +153,7 @@ const AssistantName = styled(Title)`
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
overflow: hidden;
color: #fff;
color: var(--color-white);
font-weight: 900;
`

View File

@@ -1,14 +1,17 @@
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
import { isMac, isWindows } from '@renderer/config/constant'
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore'
import { useTheme } from '@renderer/providers/ThemeProvider'
import { Assistant } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { Switch } from 'antd'
import { FC, useState } from 'react'
import styled from 'styled-components'
import Chat from './components/Chat'
import Assistants from './components/Assistants'
import { uuid } from '@renderer/utils'
import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore'
import Chat from './components/Chat'
import Navigation from './components/NavigationCenter'
import { isMac, isWindows } from '@renderer/config/constant'
import { Assistant } from '@renderer/types'
let _activeAssistant: Assistant
@@ -18,6 +21,7 @@ const HomePage: FC = () => {
const { rightSidebarShown, toggleRightSidebar } = useShowRightSidebar()
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { defaultAssistant } = useDefaultAssistant()
const { theme, toggleTheme } = useTheme()
_activeAssistant = activeAssistant
@@ -42,6 +46,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>
@@ -71,6 +81,7 @@ const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
background-color: var(--color-background);
`
export const NewButton = styled.div`
@@ -101,4 +112,12 @@ export const NewButton = styled.div`
}
`
const ThemeSwitch = styled(Switch)`
-webkit-app-region: none;
margin-right: 8px;
.icon-theme {
font-size: 14px;
}
`
export default HomePage

View File

@@ -10,7 +10,7 @@ import { droppableReorder, uuid } from '@renderer/utils'
import { Dropdown } from 'antd'
import { ItemType } from 'antd/es/menu/interface'
import { last } from 'lodash'
import { FC } from 'react'
import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -22,65 +22,78 @@ interface Props {
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
const { updateAssistant } = useAssistant(activeAssistant.id)
const generating = useAppSelector((state) => state.runtime.generating)
const { updateAssistant } = useAssistant(activeAssistant.id)
const { t } = useTranslation()
const onDelete = (assistant: Assistant) => {
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant()
removeAssistant(assistant.id)
}
const onDelete = useCallback(
(assistant: Assistant) => {
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant()
removeAssistant(assistant.id)
},
[assistants, onCreateAssistant, removeAssistant, setActiveAssistant]
)
const getMenuItems = (assistant: Assistant) =>
[
{
label: t('common.edit'),
key: 'edit',
icon: <EditOutlined />,
async onClick() {
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
const getMenuItems = useCallback(
(assistant: Assistant) =>
[
{
label: t('common.edit'),
key: 'edit',
icon: <EditOutlined />,
async onClick() {
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
}
},
{
label: t('common.duplicate'),
key: 'duplicate',
icon: <CopyOutlined />,
onClick: async () => {
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
addAssistant(_assistant)
setActiveAssistant(_assistant)
}
},
{ type: 'divider' },
{
label: t('common.delete'),
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => onDelete(assistant)
}
},
{
label: t('common.duplicate'),
key: 'duplicate',
icon: <CopyOutlined />,
onClick: async () => {
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
addAssistant(_assistant)
setActiveAssistant(_assistant)
}
},
{ type: 'divider' },
{
label: t('common.delete'),
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => onDelete(assistant)
] as ItemType[],
[addAssistant, onDelete, setActiveAssistant, t, updateAssistant]
)
const onDragEnd = useCallback(
(result: DropResult) => {
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
const reorderAssistants = droppableReorder<Assistant>(assistants, sourceIndex, destIndex)
updateAssistants(reorderAssistants)
}
] as ItemType[]
},
[assistants, updateAssistants]
)
const onDragEnd = (result: DropResult) => {
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
const reorderAssistants = droppableReorder<Assistant>(assistants, sourceIndex, destIndex)
updateAssistants(reorderAssistants)
}
}
const onSwitchAssistant = (assistant: Assistant) => {
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
return
}
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
setActiveAssistant(assistant)
}
const onSwitchAssistant = useCallback(
(assistant: Assistant): any => {
if (generating) {
return window.message.warning({
content: t('message.switch.disabled'),
key: 'switch-assistant'
})
}
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
setActiveAssistant(assistant)
},
[generating, setActiveAssistant, t]
)
return (
<Container>
@@ -91,12 +104,18 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
{assistants.map((assistant, index) => (
<Draggable key={`draggable_${assistant.id}_${index}`} draggableId={assistant.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
<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>
@@ -127,9 +146,9 @@ const AssistantItem = styled.div`
flex-direction: column;
padding: 7px 10px;
position: relative;
border-radius: 5px;
margin-bottom: 5px;
border-radius: 8px;
cursor: pointer;
font-family: Poppins;
.anticon {
display: none;
}
@@ -143,13 +162,15 @@ const AssistantItem = styled.div`
&.active {
background-color: var(--color-background-mute);
cursor: pointer;
.name {
font-weight: 500;
}
}
`
const AssistantName = styled.div`
font-size: 14px;
color: var(--color-text-1);
font-weight: 500;
color: var(--color-text);
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;

View File

@@ -1,12 +1,13 @@
import { Assistant } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
import Inputbar from './Inputbar'
import Messages from './Messages'
import { Flex } from 'antd'
import RightSidebar from './RightSidebar'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useActiveTopic } from '@renderer/hooks/useTopic'
import { Assistant } from '@renderer/types'
import { Flex } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
import Inputbar from './input/Inputbar'
import Messages from './Messages'
import RightSidebar from './sidebar/RightSidebar'
interface Props {
assistant: Assistant
@@ -18,10 +19,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 +36,8 @@ const Container = styled.div`
justify-content: space-between;
`
const Main = styled(Flex)`
height: calc(100vh - var(--navbar-height));
`
export default Chat

View File

@@ -1,21 +1,29 @@
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,
QuestionCircleOutlined,
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 { useRuntime } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message } from '@renderer/types'
import { firstLetter } from '@renderer/utils'
import { Avatar, Dropdown, Tooltip } from 'antd'
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
import { Avatar, Dropdown, Popconfirm, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { isEmpty, upperFirst } from 'lodash'
import { FC, useCallback } from 'react'
import { upperFirst } from 'lodash'
import { FC, memo, useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Markdown from 'react-markdown'
import styled from 'styled-components'
import CodeBlock from './CodeBlock'
import { useRuntime } from '@renderer/hooks/useStore'
import Markdown from './markdown/Markdown'
interface Props {
message: Message
@@ -31,88 +39,68 @@ 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'
const canRegenerate = isLastMessage && message.role === 'assistant'
const isAssistantMessage = message.role === 'assistant'
const canRegenerate = isLastMessage && isAssistantMessage
const onCopy = () => {
const onCopy = useCallback(() => {
navigator.clipboard.writeText(message.content)
window.message.success({ content: t('message.copied'), key: 'copy-message' })
}
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}, [message.content, t])
const onDelete = async () => {
const confirmed = await window.modal.confirm({
icon: null,
title: t('message.message.delete.title'),
content: t('message.message.delete.content'),
okText: t('common.delete'),
okType: 'danger'
})
confirmed && onDeleteMessage?.(message)
}
const onEdit = useCallback(() => EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, message), [message])
const onEdit = () => {
EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, message)
}
const onRegenerate = () => {
const onRegenerate = useCallback(() => {
onDeleteMessage?.(message)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE), 100)
}
const getMessageContent = (message: Message) => {
if (isEmpty(message.content) && message.status === 'paused') {
return t('message.chat.completion.paused')
}
return message.content
}
}, [message, onDeleteMessage])
const getUserName = useCallback(() => {
if (message.id === 'assistant') {
return assistant.name
}
if (message.role === 'assistant') {
return upperFirst(message.modelId)
}
if (message.id === 'assistant') return assistant?.name
if (message.role === 'assistant') return upperFirst(message.modelId)
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) => {
return [
{
label: t('chat.save'),
key: 'save',
icon: <SaveOutlined />,
onClick: () => {
const fileName = message.createdAt + '.md'
window.api.saveFile(fileName, message.content)
}
}
]
},
[t]
)
const fontFamily = messageFont === 'serif' ? "Georgia, Cambria, 'Times New Roman', Times, serif" : undefined
const serifFonts = "Georgia, Cambria, 'Times New Roman', Times, serif"
const fontFamily = messageFont === 'serif' ? serifFonts : 'Poppins, -apple-system, sans-serif'
const messageBorder = showMessageDivider ? undefined : 'none'
const avatarSource = useMemo(() => (message.modelId ? getModelLogo(message.modelId) : undefined), [message.modelId])
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const dropdownItems = useMemo(
() => [
{
label: t('chat.save'),
key: 'save',
icon: <SaveOutlined />,
onClick: () => {
const fileName = message.createdAt + '.md'
window.api.saveFile(fileName, message.content)
}
}
],
[t, message]
)
return (
<MessageContainer key={message.id} className="message" style={{ border: messageBorder }}>
<MessageHeader>
<AvatarWrapper>
{message.role === 'assistant' ? (
<Avatar src={message.modelId ? getModelLogo(message.modelId) : Logo} size={35}>
{firstLetter(message.modelId).toUpperCase()}
{isAssistantMessage ? (
<Avatar src={avatarSource} size={35}>
{avatarName}
</Avatar>
) : (
<Avatar src={avatar} size={35} />
)}
<UserWrap>
<UserName>{getUserName()}</UserName>
<UserName>{username}</UserName>
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</UserWrap>
</AvatarWrapper>
@@ -123,11 +111,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
<SyncOutlined spin size={24} />
</MessageContentLoading>
)}
{message.status !== 'sending' && (
<Markdown className="markdown" components={{ code: CodeBlock as any }}>
{getMessageContent(message)}
</Markdown>
)}
{message.status !== 'sending' && <Markdown message={message} />}
{message.usage && !generating && (
<MessageMetadata>
Tokens: {message.usage.total_tokens} | {message.usage.prompt_tokens}{message.usage.completion_tokens}
@@ -137,30 +121,37 @@ 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>
</Tooltip>
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
<ActionButton>
<DeleteOutlined onClick={onDelete} />
<ActionButton onClick={onCopy}>
{!copied && <CopyOutlined />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</ActionButton>
</Tooltip>
<Popconfirm
title={t('message.message.delete.content')}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onConfirm={() => onDeleteMessage?.(message)}>
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
<ActionButton>
<DeleteOutlined />
</ActionButton>
</Tooltip>
</Popconfirm>
{canRegenerate && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton>
<SyncOutlined onClick={onRegenerate} />
<ActionButton onClick={onRegenerate}>
<SyncOutlined />
</ActionButton>
</Tooltip>
)}
{!isUserMessage && (
<Dropdown menu={{ items: getDropdownMenus(message) }} trigger={['click']} placement="topRight" arrow>
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
<ActionButton>
<MenuOutlined />
</ActionButton>
@@ -176,7 +167,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const MessageContainer = styled.div`
display: flex;
flex-direction: column;
padding: 10px;
padding: 10px 20px;
position: relative;
border-bottom: 0.5px dotted var(--color-border);
.menubar {
@@ -280,4 +271,4 @@ const ActionButton = styled.div`
}
`
export default MessageItem
export default memo(MessageItem)

View File

@@ -1,16 +1,18 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Message, Topic } from '@renderer/types'
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 { 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 { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import LocalStorage from '@renderer/services/storage'
import { Assistant, Message, Topic } from '@renderer/types'
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
import { t } from 'i18next'
import localforage from 'localforage'
import { debounce, reverse } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import styled from 'styled-components'
import MessageItem from './Message'
import Suggestions from './Suggestions'
interface Props {
assistant: Assistant
@@ -20,28 +22,28 @@ interface Props {
const Messages: FC<Props> = ({ assistant, topic }) => {
const [messages, setMessages] = useState<Message[]>([])
const [lastMessage, setLastMessage] = useState<Message | null>(null)
const { updateTopic } = useAssistant(assistant.id)
const provider = useProviderByAssistant(assistant)
const containerRef = useRef<HTMLDivElement>(null)
const { updateTopic } = useAssistant(assistant.id)
const assistantDefaultMessage: Message = {
id: 'assistant',
role: 'assistant',
content: assistant.description || assistant.prompt || t('assistant.default.description'),
assistantId: assistant.id,
topicId: topic.id,
status: 'pending',
createdAt: new Date().toISOString()
}
const assistantDefaultMessage: Message = useMemo(
() => ({
id: 'assistant',
role: 'assistant',
content: assistant.description || assistant.prompt || t('assistant.default.description'),
assistantId: assistant.id,
topicId: topic.id,
status: 'pending',
createdAt: new Date().toISOString()
}),
[assistant.description, assistant.id, assistant.prompt, topic.id]
)
const onSendMessage = useCallback(
(message: Message) => {
const _messages = [...messages, message]
setMessages(_messages)
localforage.setItem(`topic:${topic.id}`, {
...topic,
messages: _messages
})
localforage.setItem(`topic:${topic.id}`, { ...topic, messages: _messages })
},
[messages, topic]
)
@@ -53,14 +55,14 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
}
}, [assistant, messages, topic, updateTopic])
const onDeleteMessage = (message: Message) => {
const _messages = messages.filter((m) => m.id !== message.id)
setMessages(_messages)
localforage.setItem(`topic:${topic.id}`, {
id: topic.id,
messages: _messages
})
}
const onDeleteMessage = useCallback(
(message: Message) => {
const _messages = messages.filter((m) => m.id !== message.id)
setMessages(_messages)
localforage.setItem(`topic:${topic.id}`, { id: topic.id, messages: _messages })
},
[messages, topic.id]
)
useEffect(() => {
const unsubscribes = [
@@ -84,18 +86,23 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
})
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [assistant, autoRenameTopic, messages, onSendMessage, provider, topic, updateTopic])
}, [assistant, messages, provider, topic, autoRenameTopic, updateTopic, onSendMessage])
useEffect(() => {
runAsyncFunction(async () => {
const messages = await LocalStorage.getTopicMessages(topic.id)
setMessages(messages || [])
})
runAsyncFunction(async () => setMessages((await LocalStorage.getTopicMessages(topic.id)) || []))
}, [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])
scrollTop()
}, [messages, lastMessage, scrollTop])
useEffect(() => {
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
@@ -103,6 +110,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,11 +126,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;
}
`
export default Messages

View File

@@ -1,16 +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 { getModelLogo } from '@renderer/config/provider'
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 { upperFirst } from 'lodash'
import { removeLeadingEmoji } from '@renderer/utils'
import { Avatar, Button, Dropdown, MenuProps } from 'antd'
import { first, upperFirst } from 'lodash'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { NewButton } from '../HomePage'
interface Props {
@@ -31,22 +33,30 @@ const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group',
children: p.models.map((m) => ({
key: m.id,
label: upperFirst(m.name),
style: m.id === model?.id ? { color: colorPrimary } : undefined,
onClick: () => setModel(m)
key: m?.id,
label: upperFirst(m?.name),
style: m?.id === model?.id ? { color: 'var(--color-primary)' } : undefined,
icon: (
<Avatar src={getModelLogo(m?.id || '')} size={24}>
{first(m?.name)}
</Avatar>
),
onClick: () => m && setModel(m)
}))
}))
return (
<NavbarCenter style={{ paddingLeft: isMac ? 16 : 8 }}>
{!showAssistants && (
<NewButton onClick={toggleShowAssistants} style={{ marginRight: 8 }}>
<NewButton onClick={toggleShowAssistants} style={{ marginRight: isMac ? 8 : 25 }}>
<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 +77,14 @@ const AssistantName = styled.span`
`
const DropdownButton = styled(Button)`
font-size: 10px;
font-size: 11px;
border-radius: 15px;
padding: 0 8px;
`
const ModelName = styled.span`
margin-left: -2px;
font-weight: bolder;
`
export default NavigationCenter

View File

@@ -0,0 +1,119 @@
import { fetchSuggestions } from '@renderer/services/api'
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 BeatLoader from 'react-spinners/BeatLoader'
import styled from 'styled-components'
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 <Container style={{ paddingBottom: 10 }} />
}
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: 10px 10px 20px 65px;
display: flex;
width: 100%;
flex-direction: row;
flex-wrap: wrap;
gap: 15px;
`
const SuggestionsContainer = styled.div`
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
`
const SuggestionItem = styled.div`
display: flex;
align-items: center;
width: fit-content;
padding: 5px 10px;
border-radius: 12px;
font-size: 12px;
color: var(--color-text);
background: var(--color-background-mute);
cursor: pointer;
&:hover {
opacity: 0.9;
}
`
export default Suggestions

View File

@@ -1,145 +0,0 @@
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { fetchMessagesSummary } from '@renderer/services/api'
import { Assistant, Topic } from '@renderer/types'
import { Dropdown, MenuProps } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
import { DeleteOutlined, EditOutlined, SignatureOutlined } from '@ant-design/icons'
import LocalStorage from '@renderer/services/storage'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import { droppableReorder } from '@renderer/utils'
import { useTranslation } from 'react-i18next'
import { useAppSelector } from '@renderer/store'
interface Props {
assistant: Assistant
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
}
const TopicsTab: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
const { rightSidebarShown } = useShowRightSidebar()
const { assistant, removeTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
const { t } = useTranslation()
const generating = useAppSelector((state) => state.runtime.generating)
const getTopicMenuItems = (topic: Topic) => {
const menus: MenuProps['items'] = [
{
label: t('assistant.topics.auto_rename'),
key: 'auto-rename',
icon: <SignatureOutlined />,
async onClick() {
const messages = await LocalStorage.getTopicMessages(topic.id)
if (messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant })
if (summaryText) {
updateTopic({ ...topic, name: summaryText })
}
}
}
},
{
label: t('common.rename'),
key: 'rename',
icon: <EditOutlined />,
async onClick() {
const name = await PromptPopup.show({
title: t('assistant.topics.edit.title'),
message: t('assistant.topics.edit.placeholder'),
defaultValue: topic?.name || ''
})
if (name && topic?.name !== name) {
updateTopic({ ...topic, name })
}
}
}
]
if (assistant.topics.length > 1) {
menus.push({ type: 'divider' })
menus.push({
label: t('common.delete'),
danger: true,
key: 'delete',
icon: <DeleteOutlined />,
onClick() {
if (assistant.topics.length === 1) return
removeTopic(topic)
setActiveTopic(assistant.topics[0])
}
})
}
return menus
}
const onDragEnd = (result: DropResult) => {
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
updateTopics(droppableReorder(assistant.topics, sourceIndex, destIndex))
}
}
const onSwitchTopic = (topic: Topic) => {
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
return
}
setActiveTopic(topic)
}
return (
<Container style={{ display: rightSidebarShown ? 'block' : 'none' }}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{assistant.topics.map((topic, index) => (
<Draggable key={`draggable_${topic.id}_${index}`} draggableId={topic.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
<TopicListItem
className={topic.id === activeTopic?.id ? 'active' : ''}
onClick={() => onSwitchTopic(topic)}>
{topic.name}
</TopicListItem>
</Dropdown>
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
</Container>
)
}
const Container = styled.div`
padding: 15px 10px;
`
const TopicListItem = styled.div`
padding: 8px 10px;
margin-bottom: 5px;
cursor: pointer;
border-radius: 5px;
font-size: 13px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
background-color: var(--color-background-soft);
}
`
export default TopicsTab

View File

@@ -4,10 +4,11 @@ import {
FullscreenExitOutlined,
FullscreenOutlined,
HistoryOutlined,
MoreOutlined,
PauseCircleOutlined,
PlusCircleOutlined
PlusCircleOutlined,
QuestionCircleOutlined
} 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 +24,8 @@ 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
@@ -41,7 +42,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const inputRef = useRef<TextAreaRef>(null)
const { t } = useTranslation()
const sendMessage = () => {
const sendMessage = useCallback(() => {
if (generating) {
return
}
@@ -63,11 +64,23 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
setText('')
}
setExpend(false)
}, [assistant.id, assistant.topics, generating, text])
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (expended) {
if (event.key === 'Escape') {
return setExpend(false)
}
if (event.key === 'Enter' && event.shiftKey) {
return sendMessage()
}
return
}
if (sendMessageShortcut === 'Enter' && event.key === 'Enter') {
if (event.shiftKey) {
return
@@ -127,8 +140,8 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}, [assistant])
return (
<Container id="inputbar" style={{ minHeight: expended ? '35%' : 'var(--input-bar-height)' }}>
<Toolbar>
<Container id="inputbar" style={{ minHeight: expended ? '100%' : 'var(--input-bar-height)' }}>
<Toolbar onDoubleClick={() => setExpend(!expended)}>
<ToolbarMenu>
<Tooltip placement="top" title={t('assistant.input.new_chat')} arrow>
<ToolbarButton type="text" onClick={addNewTopic}>
@@ -137,11 +150,11 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.clear')} arrow>
<Popconfirm
icon={false}
title={t('assistant.input.clear.title')}
description={t('assistant.input.clear.content')}
title={t('assistant.input.clear.content')}
placement="top"
onConfirm={clearTopic}
okButtonProps={{ danger: true }}
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
okText={t('assistant.input.clear')}>
<ToolbarButton type="text">
<ClearOutlined />
@@ -163,6 +176,12 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
</ToolbarButton>
</Tooltip>
{showInputEstimatedTokens && (
<TextCount>
<HistoryOutlined /> {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} | T
{`${inputTokenCount}/${estimateTokenCount}`}
</TextCount>
)}
</ToolbarMenu>
<ToolbarMenu>
{generating && (
@@ -172,11 +191,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</ToolbarButton>
</Tooltip>
)}
<SendMessageSetting>
<ToolbarButton type="text" style={{ marginRight: 0 }}>
<MoreOutlined />
</ToolbarButton>
</SendMessageSetting>
<SendMessageButton sendMessage={sendMessage} />
</ToolbarMenu>
</Toolbar>
<Textarea
@@ -187,16 +202,9 @@ 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>
)}
</Container>
)
}
@@ -205,9 +213,7 @@ const Container = styled.div`
display: flex;
flex-direction: column;
width: 100%;
height: var(--input-bar-height);
border-top: 0.5px solid var(--color-border);
padding: 5px 15px;
transition: all 0.3s ease;
position: relative;
`
@@ -217,20 +223,21 @@ const Textarea = styled(TextArea)`
border-radius: 0;
display: flex;
flex: 1;
margin: 0 15px 5px 15px;
`
const Toolbar = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
margin: 0 -5px;
margin-bottom: 5px;
padding: 3px 10px;
`
const ToolbarMenu = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 6px;
`
const ToolbarButton = styled(Button)`
@@ -239,7 +246,6 @@ const ToolbarButton = styled(Button)`
font-size: 18px;
border-radius: 50%;
transition: all 0.3s ease;
margin-right: 6px;
color: var(--color-icon);
&.anticon {
transition: all 0.3s ease;
@@ -248,22 +254,19 @@ const ToolbarButton = styled(Button)`
&:hover {
background-color: var(--color-background-soft);
.anticon {
color: white;
color: var(--color-text-1);
}
}
`
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;
padding: 2px;
border-top-left-radius: 7px;
user-select: none;
margin-right: 10px;
`
export default Inputbar

View File

@@ -1,12 +1,15 @@
import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons'
import { SendOutlined } from '@ant-design/icons'
import { useSettings } from '@renderer/hooks/useSettings'
import { Dropdown, MenuProps } from 'antd'
import { FC, PropsWithChildren } from 'react'
import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
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,17 @@ const SendMessageSetting: FC<Props> = ({ children }) => {
]
return (
<Dropdown
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
placement="topRight"
<Dropdown.Button
size="small"
onClick={sendMessage}
trigger={['click']}
arrow>
{children}
</Dropdown>
arrow
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
style={{ width: 'auto' }}>
{t('assistant.input.send')}
<SendOutlined />
</Dropdown.Button>
)
}
export default SendMessageSetting
export default SendMessageButton

View File

@@ -1,11 +1,14 @@
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 +18,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 +39,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 +62,10 @@ const CodeHeader = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
color: #fff;
color: var(--color-text);
font-size: 14px;
font-weight: bold;
background-color: #323232;
background-color: var(--color-code-background);
height: 40px;
padding: 0 10px;
border-top-left-radius: 8px;

View File

@@ -0,0 +1,8 @@
import { omit } from 'lodash'
import React from 'react'
const Link: React.FC = (props) => {
return <a {...omit(props, 'node')} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()} />
}
export default Link

View File

@@ -0,0 +1,49 @@
import 'katex/dist/katex.min.css'
import { Message } from '@renderer/types'
import { isEmpty } from 'lodash'
import { FC, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ReactMarkdown from 'react-markdown'
import rehypeKatex from 'rehype-katex'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import CodeBlock from './CodeBlock'
import Link from './Link'
interface Props {
message: Message
}
const Markdown: FC<Props> = ({ message }) => {
const { t } = useTranslation()
const getMessageContent = useCallback(
(message: Message) => {
const empty = isEmpty(message.content)
const paused = message.status === 'paused'
return empty && paused ? t('message.chat.completion.paused') : message.content
},
[t]
)
return useMemo(() => {
return (
<ReactMarkdown
className="markdown"
remarkPlugins={[[remarkMath, { singleDollarTextMath: false }], remarkGfm]}
remarkRehypeOptions={{
footnoteLabel: t('common.footnotes'),
footnoteLabelTagName: 'h4',
footnoteBackContent: ' '
}}
rehypePlugins={[rehypeKatex]}
components={{ code: CodeBlock as any, a: Link as any }}>
{getMessageContent(message)}
</ReactMarkdown>
)
}, [getMessageContent, message, t])
}
export default Markdown

View File

@@ -1,11 +1,12 @@
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Topic } from '@renderer/types'
import { FC, useEffect, useState } from 'react'
import styled from 'styled-components'
import TopicsTab from './TopicsTab'
import SettingsTab from './SettingsTab'
import { useTranslation } from 'react-i18next'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import styled from 'styled-components'
import SettingsTab from './SettingsTab'
import TopicsTab from './TopicsTab'
interface Props {
assistant: Assistant
@@ -47,8 +48,12 @@ const RightSidebar: FC<Props> = (props) => {
return () => unsubscribes.forEach((unsub) => unsub())
}, [hideRightSidebar, isSettingsTab, isTopicTab, rightSidebarShown, showRightSidebar])
if (!rightSidebarShown) {
return null
}
return (
<Container style={{ display: rightSidebarShown ? 'block' : 'none' }}>
<Container>
<Tabs>
<Tab className={tab === 'topic' ? 'active' : ''} onClick={() => setTab('topic')}>
{t('common.topics')}
@@ -57,17 +62,20 @@ const RightSidebar: FC<Props> = (props) => {
{t('settings.title')}
</Tab>
</Tabs>
{tab === 'topic' && <TopicsTab {...props} />}
{tab === 'settings' && <SettingsTab assistant={props.assistant} />}
<TabContent>
{tab === 'topic' && <TopicsTab {...props} />}
{tab === 'settings' && <SettingsTab assistant={props.assistant} />}
</TabContent>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
width: var(--topic-list-width);
height: 100%;
height: calc(100vh - var(--navbar-height));
border-left: 0.5px solid var(--color-border);
overflow-y: auto;
.collapsed {
width: 0;
border-left: none;
@@ -90,12 +98,18 @@ const Tab = styled.div`
align-items: center;
font-size: 13px;
cursor: pointer;
color: #8a8a8a;
border-bottom: 1px solid transparent;
color: var(--color-text-3);
&.active {
color: #bbb;
color: var(--color-text-2);
font-weight: 600;
}
`
const TabContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow-y: auto;
`
export default RightSidebar

View File

@@ -1,16 +1,16 @@
import { Assistant } from '@renderer/types'
import styled from 'styled-components'
import { QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { Button, Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
import { useSettings } from '@renderer/hooks/useSettings'
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings/components'
import { useAppDispatch } from '@renderer/store'
import { setMessageFont, setShowInputEstimatedTokens, setShowMessageDivider } from '@renderer/store/settings'
import { Assistant } from '@renderer/types'
import { Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
import { debounce } from 'lodash'
import { FC, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { QuestionCircleOutlined } from '@ant-design/icons'
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings/components'
import { useSettings } from '@renderer/hooks/useSettings'
import { useAppDispatch } from '@renderer/store'
import { setMessageFont, setShowInputEstimatedTokens, setShowMessageDivider } from '@renderer/store/settings'
import styled from 'styled-components'
interface Props {
assistant: Assistant
@@ -78,15 +78,15 @@ const SettingsTab: FC<Props> = (props) => {
return (
<Container>
<SettingSubtitle style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
<SettingSubtitle>
{t('settings.messages.model.title')}{' '}
<Button size="small" onClick={onReset}>
{t('assistant.settings.reset')}
</Button>
<Tooltip title={t('assistant.settings.reset')}>
<ReloadOutlined onClick={onReset} style={{ cursor: 'pointer', fontSize: 12, padding: '0 3px' }} />
</Tooltip>
</SettingSubtitle>
<SettingDivider />
<Row align="middle">
<Label>{t('assistant.settings.conext_count')}</Label>
<Label>{t('assistant.settings.temperature')}</Label>
<Tooltip title={t('assistant.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
@@ -177,6 +177,9 @@ const SettingsTab: FC<Props> = (props) => {
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
padding: 0 15px;
`

View File

@@ -0,0 +1,159 @@
import { DeleteOutlined, EditOutlined, OpenAIOutlined } from '@ant-design/icons'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import PromptPopup from '@renderer/components/Popups/PromptPopup'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { fetchMessagesSummary } from '@renderer/services/api'
import LocalStorage from '@renderer/services/storage'
import { useAppSelector } from '@renderer/store'
import { Assistant, Topic } from '@renderer/types'
import { droppableReorder } from '@renderer/utils'
import { Dropdown, MenuProps } from 'antd'
import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
assistant: Assistant
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
}
const TopicsTab: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
const { assistant, removeTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
const { t } = useTranslation()
const generating = useAppSelector((state) => state.runtime.generating)
const getTopicMenuItems = useCallback(
(topic: Topic) => {
const menus: MenuProps['items'] = [
{
label: t('assistant.topics.auto_rename'),
key: 'auto-rename',
icon: <OpenAIOutlined />,
async onClick() {
const messages = await LocalStorage.getTopicMessages(topic.id)
if (messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant })
if (summaryText) {
updateTopic({ ...topic, name: summaryText })
}
}
}
},
{
label: t('assistant.topics.edit.title'),
key: 'rename',
icon: <EditOutlined />,
async onClick() {
const name = await PromptPopup.show({
title: t('assistant.topics.edit.title'),
message: '',
defaultValue: topic?.name || ''
})
if (name && topic?.name !== name) {
updateTopic({ ...topic, name })
}
}
}
]
if (assistant.topics.length > 1) {
menus.push({ type: 'divider' })
menus.push({
label: t('common.delete'),
danger: true,
key: 'delete',
icon: <DeleteOutlined />,
onClick() {
if (assistant.topics.length === 1) return
removeTopic(topic)
setActiveTopic(assistant.topics[0])
}
})
}
return menus
},
[assistant, removeTopic, setActiveTopic, t, updateTopic]
)
const onDragEnd = useCallback(
(result: DropResult) => {
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
updateTopics(droppableReorder(assistant.topics, sourceIndex, destIndex))
}
},
[assistant.topics, updateTopics]
)
const onSwitchTopic = useCallback(
(topic: Topic) => {
if (generating) {
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
return
}
setActiveTopic(topic)
},
[generating, setActiveTopic, t]
)
return (
<Container>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
{assistant.topics.map((topic, index) => (
<Draggable key={`draggable_${topic.id}_${index}`} draggableId={topic.id} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
<TopicListItem
className={topic.id === activeTopic?.id ? 'active' : ''}
onClick={() => onSwitchTopic(topic)}>
{topic.name}
</TopicListItem>
</Dropdown>
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
</Container>
)
}
const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
padding: 15px 10px;
`
const TopicListItem = styled.div`
padding: 7px 10px;
cursor: pointer;
border-radius: 8px;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-family: Poppins;
&:hover {
background-color: var(--color-background-soft);
}
&.active {
background-color: var(--color-background-mute);
}
`
export default TopicsTab

View File

@@ -1,11 +1,12 @@
import { Avatar, Button, Progress, Row, Tag } from 'antd'
import { FC, useEffect, useState } from 'react'
import styled from 'styled-components'
import Logo from '@renderer/assets/images/logo.png'
import { runAsyncFunction } from '@renderer/utils'
import { useTranslation } from 'react-i18next'
import { debounce } from 'lodash'
import { Avatar, Button, Progress, Row, Tag } from 'antd'
import { ProgressInfo } from 'electron-updater'
import { debounce } from 'lodash'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
const AboutSettings: FC = () => {
@@ -39,6 +40,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 +126,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 +136,11 @@ const AboutSettings: FC = () => {
</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.license.title')}</SettingRowTitle>
<Button onClick={showLicense}>{t('settings.about.license.button')}</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.contact.title')}</SettingRowTitle>
<Button onClick={mailto}>{t('settings.about.contact.button')}</Button>

View File

@@ -3,11 +3,12 @@ import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/const
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { Button, Col, Input, InputNumber, Row, Slider, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { debounce } from 'lodash'
import { FC, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from './components'
import { debounce } from 'lodash'
const AssistantSettings: FC = () => {
const { defaultAssistant, updateDefaultAssistant } = useDefaultAssistant()

View File

@@ -1,28 +1,27 @@
import { FC, useState } from 'react'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
import { Avatar, Input, Select, Upload } from 'antd'
import styled from 'styled-components'
import LocalStorage from '@renderer/services/storage'
import { compressImage, isValidProxyUrl } from '@renderer/utils'
import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import LocalStorage from '@renderer/services/storage'
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 { useTranslation } from 'react-i18next'
import { setLanguage, setUserName, ThemeMode } from '@renderer/store/settings'
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
import i18n from '@renderer/i18n'
import { compressImage, isValidProxyUrl } from '@renderer/utils'
import { Avatar, Input, Select, Upload } from 'antd'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
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()
const onSelectLanguage = (value: string) => {
dispatch(setLanguage(value))
i18n.changeLanguage(value)
localStorage.setItem('language', value)
}
@@ -53,6 +52,20 @@ const GeneralSettings: FC = () => {
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
<Select
defaultValue={theme}
style={{ width: 120 }}
onChange={setTheme}
options={[
{ value: ThemeMode.light, label: t('settings.theme.light') },
{ value: ThemeMode.dark, label: t('settings.theme.dark') },
{ value: ThemeMode.auto, label: t('settings.theme.auto') }
]}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('common.avatar')}</SettingRowTitle>
<Upload

View File

@@ -1,14 +1,17 @@
import { FC } from 'react'
import { SettingContainer, SettingDivider, SettingTitle } from './components'
import { Select } from 'antd'
import { useProviders } from '@renderer/hooks/useProvider'
import { EditOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { find } from 'lodash'
import { useProviders } from '@renderer/hooks/useProvider'
import { Model } from '@renderer/types'
import { Select } from 'antd'
import { find } from 'lodash'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { SettingContainer, SettingDivider, SettingTitle } from './components'
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 +27,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 +45,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 +58,21 @@ const ModelSettings: FC = () => {
onChange={(id) => setTopicNamingModel(find(allModels, { id }) as Model)}
options={selectOptions}
/>
<div style={{ height: 30 }} />
<SettingTitle>
<div>
<TranslationOutlined style={iconStyle} />
{t('settings.models.translate_model')}
</div>
</SettingTitle>
<SettingDivider />
<Select
defaultValue={translateModel?.id}
style={{ width: 200 }}
onChange={(id) => setTranslateModel(find(allModels, { id }) as Model)}
options={selectOptions}
placeholder={t('settings.models.empty')}
/>
</SettingContainer>
)
}

View File

@@ -1,16 +1,17 @@
import { PlusOutlined } from '@ant-design/icons'
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
import { getProviderLogo } from '@renderer/config/provider'
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
import { Provider } from '@renderer/types'
import { droppableReorder, generateColorFromChar, getFirstCharacter, uuid } from '@renderer/utils'
import { Avatar, Button, Dropdown, MenuProps, Tag } from 'antd'
import { FC, useState } from 'react'
import styled from 'styled-components'
import ProviderSetting from './components/ProviderSetting'
import { useTranslation } from 'react-i18next'
import { PlusOutlined } from '@ant-design/icons'
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
import styled from 'styled-components'
import AddProviderPopup from './components/AddProviderPopup'
import ProviderSetting from './components/ProviderSetting'
const ProviderSettings: FC = () => {
const providers = useAllProviders()
@@ -42,7 +43,7 @@ const ProviderSettings: FC = () => {
apiKey: '',
apiHost: '',
models: [],
enabled: false,
enabled: true,
isSystem: false
} as Provider
addProvider(provider)
@@ -92,7 +93,11 @@ const ProviderSettings: FC = () => {
{providers.map((provider, index) => (
<Draggable key={`draggable_${provider.id}_${index}`} draggableId={provider.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
<Dropdown
menu={{ items: provider.isSystem ? [] : getDropdownMenus(provider) }}
trigger={['contextMenu']}>
@@ -100,11 +105,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 +156,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;
`
@@ -166,27 +171,27 @@ const ProviderListItem = styled.div`
flex-direction: row;
align-items: center;
padding: 5px 8px;
margin-bottom: 5px;
width: 100%;
cursor: pointer;
border-radius: 5px;
font-size: 14px;
transition: all 0.2s ease-in-out;
&:hover {
background: #135200;
background: var(--color-primary-mute);
}
&.active {
background: #135200;
font-weight: bold;
background: var(--color-primary);
color: var(--color-white);
font-weight: bold !important;
}
`
const ProviderItemName = styled.div`
margin-left: 10px;
font-weight: bold;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
font-weight: 500;
`
const AddButtonWrapper = styled.div`

View File

@@ -1,13 +1,14 @@
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import { Link, Route, Routes, useLocation } from 'react-router-dom'
import styled from 'styled-components'
import GeneralSettings from './GeneralSettings'
import AboutSettings from './AboutSettings'
import AssistantSettings from './AssistantSettings'
import GeneralSettings from './GeneralSettings'
import ModelSettings from './ModelSettings'
import ProviderSettings from './ProviderSettings'
import { useTranslation } from 'react-i18next'
const SettingsPage: FC = () => {
const { pathname } = useLocation()
@@ -62,13 +63,14 @@ const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
background-color: var(--color-background);
`
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 +86,14 @@ const MenuItem = styled.li`
cursor: pointer;
border-radius: 5px;
font-size: 14px;
font-weight: 500;
transition: all 0.2s ease-in-out;
&:hover {
background: #135200;
background: var(--color-primary-soft);
}
&.active {
background: #135200;
font-weight: bold;
background: var(--color-primary);
color: var(--color-white);
}
`

View File

@@ -1,17 +1,18 @@
import { LoadingOutlined, MinusOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
import { SYSTEM_MODELS } from '@renderer/config/models'
import { getModelLogo } from '@renderer/config/provider'
import { useProvider } from '@renderer/hooks/useProvider'
import { fetchModels } from '@renderer/services/api'
import { getModelLogo } from '@renderer/config/provider'
import { Model, Provider } from '@renderer/types'
import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/utils'
import { Avatar, Button, Empty, Flex, Modal, Tag } from 'antd'
import Search from 'antd/es/input/Search'
import { groupBy, isEmpty, uniqBy } from 'lodash'
import { useEffect, useState } from 'react'
import styled from 'styled-components'
import { TopView } from '../../../components/TopView'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { TopView } from '../../../components/TopView'
interface ShowParams {
provider: Provider
@@ -71,7 +72,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 +180,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 +202,14 @@ const ListItemHeader = styled.div`
`
const ListItemName = styled.div`
color: #fff;
color: var(--color-text);
font-size: 14px;
font-weight: 600;
margin-left: 6px;
`
const ModelHeaderTitle = styled.div`
color: #fff;
color: var(--color-text);
font-size: 18px;
font-weight: 600;
margin-right: 10px;

View File

@@ -1,11 +1,3 @@
import { Provider } from '@renderer/types'
import { FC, useEffect, useState } from 'react'
import styled from 'styled-components'
import { Avatar, Button, Card, Divider, Flex, Input, Space, Switch } from 'antd'
import { useProvider } from '@renderer/hooks/useProvider'
import { groupBy } from 'lodash'
import { SettingContainer, SettingSubtitle, SettingTitle } from '.'
import { getModelLogo } from '@renderer/config/provider'
import {
CheckOutlined,
EditOutlined,
@@ -14,12 +6,22 @@ import {
MinusCircleOutlined,
PlusOutlined
} from '@ant-design/icons'
import { getModelLogo } from '@renderer/config/provider'
import { PROVIDER_CONFIG } from '@renderer/config/provider'
import { useProvider } from '@renderer/hooks/useProvider'
import { useTheme } from '@renderer/providers/ThemeProvider'
import { checkApi } from '@renderer/services/api'
import { Provider } from '@renderer/types'
import { Avatar, Button, Card, Divider, Flex, Input, Space, Switch } from 'antd'
import Link from 'antd/es/typography/Link'
import { groupBy } from 'lodash'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingContainer, SettingSubtitle, SettingTitle } from '.'
import AddModelPopup from './AddModelPopup'
import EditModelsPopup from './EditModelsPopup'
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'
interface Props {
provider: Provider
@@ -33,6 +35,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 +71,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 +191,8 @@ const HelpTextRow = styled.div`
const HelpText = styled.div`
font-size: 11px;
color: #ffffff50;
color: var(--color-text);
opacity: 0.4;
`
const HelpLink = styled(Link)`

View File

@@ -26,7 +26,7 @@ export const SettingTitle = styled.div`
export const SettingSubtitle = styled.div`
font-size: 14px;
color: var(--color-text-2);
margin: 15px 0 10px 0;
margin: 15px 0 0 0;
user-select: none;
`

View File

@@ -0,0 +1,307 @@
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;
background-color: var(--color-background);
`
const MenuContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 15px;
gap: 20px;
`
const TranslateInputWrapper = styled.div`
display: flex;
flex-direction: row;
min-height: 350px;
gap: 20px;
`
const InputContainer = styled.div`
position: relative;
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
border: 1px solid var(--color-border);
border-radius: 10px;
`
const Textarea = styled(TextArea)`
display: flex;
flex: 1;
padding: 20px;
font-size: 16px;
overflow: auto;
.ant-input {
resize: none;
padding: 15px 20px;
}
`
const OutputContainer = styled.div`
position: relative;
display: flex;
flex: 1;
flex-direction: column;
height: 100%;
padding: 10px;
background-color: var(--color-background-soft);
border-radius: 10px;
`
const OutputText = styled.div`
padding: 5px 10px;
max-height: calc(100vh - var(--navbar-height) - 120px);
overflow: auto;
white-space: pre-wrap;
`
const TranslateButton = styled(Button)`
position: absolute;
right: 15px;
bottom: 15px;
z-index: 10;
`
const CopyButton = styled(Button)`
position: absolute;
right: 15px;
bottom: 15px;
`
export default TranslatePage

View File

@@ -0,0 +1,38 @@
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: 8
}
}}>
{children}
</ConfigProvider>
)
}
function getAntdLocale(language: string) {
switch (language) {
case 'zh-CN':
return zhCN
case 'en-US':
return undefined
default:
return zhCN
}
}
export default AntdProvider

View File

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

View File

@@ -1,12 +1,13 @@
import { Assistant, Message, Provider } from '@renderer/types'
import OpenAI from 'openai'
import Anthropic from '@anthropic-ai/sdk'
import { getDefaultModel, getTopNamingModel } from './assistant'
import { ChatCompletionCreateParamsNonStreaming, ChatCompletionMessageParam } from 'openai/resources'
import { sum, takeRight } from 'lodash'
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
import { EVENT_NAMES } from './event'
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
import { getAssistantSettings, removeQuotes } from '@renderer/utils'
import { sum, takeRight } from 'lodash'
import OpenAI from 'openai'
import { ChatCompletionCreateParamsNonStreaming, ChatCompletionMessageParam } from 'openai/resources'
import { getDefaultModel, getTopNamingModel } from './assistant'
import { EVENT_NAMES } from './event'
export default class ProviderSDK {
provider: Provider
@@ -73,6 +74,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 +135,28 @@ export default class ProviderSDK {
}
}
public async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> {
const model = assistant.model
if (!model) {
return []
}
const response: any = await this.openaiSdk.request({
method: 'post',
path: '/advice_questions',
body: {
messages: messages.filter((m) => m.role === 'user').map((m) => ({ role: m.role, content: m.content })),
model: model.id,
max_tokens: 0,
temperature: 0,
n: 0
}
})
return response?.questions?.filter(Boolean)?.map((q: any) => ({ content: q })) || []
}
public async check(): Promise<{ valid: boolean; error: Error | null }> {
const model = this.provider.models[0]
const body = {

View File

@@ -1,10 +1,18 @@
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 { isEmpty } from 'lodash'
import {
getAssistantProvider,
getDefaultModel,
getProviderByModel,
getTopNamingModel,
getTranslateModel
} from './assistant'
import { EVENT_NAMES, EventEmitter } from './event'
import ProviderSDK from './ProviderSDK'
@@ -63,11 +71,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 +110,38 @@ 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 []
}
if (model.id.endsWith('global')) {
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 +176,12 @@ export async function checkApi(provider: Provider) {
return valid
}
function hasApiKey(provider: Provider) {
if (!provider) return false
if (provider.id === 'ollama') return true
return !isEmpty(provider.apiKey)
}
export async function fetchModels(provider: Provider) {
const providerSdk = new ProviderSDK(provider)

View File

@@ -1,7 +1,7 @@
import { Assistant, Model, Provider, Topic } from '@renderer/types'
import store from '@renderer/store'
import { uuid } from '@renderer/utils'
import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { Assistant, Model, Provider, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
export function getDefaultAssistant(): Assistant {
return {
@@ -32,7 +32,11 @@ export function getTopNamingModel() {
return store.getState().llm.topicNamingModel
}
export function getAssistantProvider(assistant: Assistant) {
export function getTranslateModel() {
return store.getState().llm.translateModel
}
export function getAssistantProvider(assistant: Assistant): Provider {
const providers = store.getState().llm.providers
const provider = providers.find((p) => p.id === assistant.model?.provider)
return provider || getDefaultProvider()

View File

@@ -8,6 +8,7 @@ export default class LocalStorage {
static async getTopic(id: string) {
return localforage.getItem<Topic>(`topic:${id}`)
}
static async getTopicMessages(id: string) {
const topic = await this.getTopic(id)
return topic ? topic.messages : []

View File

@@ -2,11 +2,12 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { useDispatch, useSelector, useStore } from 'react-redux'
import { FLUSH, PAUSE, PERSIST, persistReducer, persistStore, PURGE, REGISTER, REHYDRATE } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
import assistants from './assistants'
import settings from './settings'
import llm from './llm'
import runtime from './runtime'
import migrate from './migrate'
import runtime from './runtime'
import settings from './settings'
const rootReducer = combineReducers({
assistants,
@@ -19,7 +20,7 @@ const persistedReducer = persistReducer(
{
key: 'cherry-studio',
storage,
version: 16,
version: 17,
blacklist: ['runtime'],
migrate
},

View File

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

View File

@@ -1,9 +1,10 @@
import { createMigrate } from 'redux-persist'
import { RootState } from '.'
import { SYSTEM_MODELS } from '@renderer/config/models'
import { isEmpty } from 'lodash'
import i18n from '@renderer/i18n'
import { Assistant } from '@renderer/types'
import { isEmpty } from 'lodash'
import { createMigrate } from 'redux-persist'
import { RootState } from '.'
const migrateConfig = {
'2': (state: RootState) => {
@@ -261,6 +262,15 @@ const migrateConfig = {
showInputEstimatedTokens: false
}
}
},
'17': (state: RootState) => {
return {
...state,
settings: {
...state.settings,
theme: 'auto'
}
}
}
}

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
import { v4 as uuidv4 } from 'uuid'
import imageCompression from 'browser-image-compression'
import { Assistant, AssistantSettings, Message, Model } from '@renderer/types'
import { GPTTokens } from 'gpt-tokens'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { Assistant, AssistantSettings, Message, Model } from '@renderer/types'
import imageCompression from 'browser-image-compression'
import { GPTTokens } from 'gpt-tokens'
import { takeRight } from 'lodash'
import { v4 as uuidv4 } from 'uuid'
export const runAsyncFunction = async (fn: () => void) => {
await fn()
@@ -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) {
@@ -136,7 +141,7 @@ export function removeQuotes(str) {
return str.replace(/['"]+/g, '')
}
export function generateColorFromChar(char) {
export function generateColorFromChar(char: string) {
// 使用字符的Unicode值作为随机种子
const seed = char.charCodeAt(0)

View File

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

675
yarn.lock
View File

@@ -2139,6 +2139,13 @@ __metadata:
languageName: node
linkType: hard
"@types/katex@npm:^0.16.0":
version: 0.16.7
resolution: "@types/katex@npm:0.16.7"
checksum: 10c0/68dcb9f68a90513ec78ca0196a142e15c2a2c270b1520d752bafd47a99207115085a64087b50140359017d7e9c870b3c68e7e4d36668c9e348a9ef0c48919b5a
languageName: node
linkType: hard
"@types/keygrip@npm:*":
version: 1.0.6
resolution: "@types/keygrip@npm:1.0.6"
@@ -2710,6 +2717,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 +2752,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 +3083,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 +3464,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"
@@ -3431,6 +3472,7 @@ __metadata:
eslint: "npm:^8.56.0"
eslint-plugin-react: "npm:^7.34.3"
eslint-plugin-react-hooks: "npm:^4.6.2"
eslint-plugin-simple-import-sort: "npm:^12.1.1"
eslint-plugin-unused-imports: "npm:^4.0.0"
gpt-tokens: "npm:^1.3.6"
i18next: "npm:^23.11.5"
@@ -3445,8 +3487,12 @@ __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"
rehype-katex: "npm:^7.0.0"
remark-gfm: "npm:^4.0.0"
remark-math: "npm:^6.0.0"
sass: "npm:^1.77.2"
styled-components: "npm:^6.1.11"
typescript: "npm:^5.3.3"
@@ -3611,6 +3657,13 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^8.3.0":
version: 8.3.0
resolution: "commander@npm:8.3.0"
checksum: 10c0/8b043bb8322ea1c39664a1598a95e0495bfe4ca2fad0d84a92d7d1d8d213e2a155b441d2470c8e08de7c4a28cf2bc6e169211c49e1b21d9f7edc6ae4d9356060
languageName: node
linkType: hard
"compare-version@npm:^0.1.2":
version: 0.1.2
resolution: "compare-version@npm:0.1.2"
@@ -3632,6 +3685,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 +3837,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 +4041,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 +4166,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 +4283,14 @@ __metadata:
languageName: node
linkType: hard
"env-paths@npm:^2.2.0":
"entities@npm:^4.4.0":
version: 4.5.0
resolution: "entities@npm:4.5.0"
checksum: 10c0/5b039739f7621f5d1ad996715e53d964035f75ad3b9a4d38c6b3804bb226e282ffeae2443624d8fdd9c47d8e926ae9ac009c54671243f0c3294c26af7cc85250
languageName: node
linkType: hard
"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
@@ -4518,6 +4624,13 @@ __metadata:
languageName: node
linkType: hard
"escape-string-regexp@npm:^5.0.0":
version: 5.0.0
resolution: "escape-string-regexp@npm:5.0.0"
checksum: 10c0/6366f474c6f37a802800a435232395e04e9885919873e382b157ab7e8f0feb8fed71497f84a6f6a81a49aab41815522f5839112bd38026d203aea0c91622df95
languageName: node
linkType: hard
"eslint-config-prettier@npm:^9.1.0":
version: 9.1.0
resolution: "eslint-config-prettier@npm:9.1.0"
@@ -4586,6 +4699,15 @@ __metadata:
languageName: node
linkType: hard
"eslint-plugin-simple-import-sort@npm:^12.1.1":
version: 12.1.1
resolution: "eslint-plugin-simple-import-sort@npm:12.1.1"
peerDependencies:
eslint: ">=5.0.0"
checksum: 10c0/0ad1907ad9ddbadd1db655db0a9d0b77076e274b793a77b982c8525d808d868e6ecfce24f3a411e8a1fa551077387f9ebb38c00956073970ebd7ee6a029ce2b3
languageName: node
linkType: hard
"eslint-plugin-unused-imports@npm:^4.0.0":
version: 4.0.0
resolution: "eslint-plugin-unused-imports@npm:4.0.0"
@@ -4809,6 +4931,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 +4992,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"
@@ -5328,6 +5466,68 @@ __metadata:
languageName: node
linkType: hard
"hast-util-from-dom@npm:^5.0.0":
version: 5.0.0
resolution: "hast-util-from-dom@npm:5.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
hastscript: "npm:^8.0.0"
web-namespaces: "npm:^2.0.0"
checksum: 10c0/1b0a9d65eb8f8cd3616559190bb6db271b7b4f72a13c5dc16abac264b6f7145beb408fbaa497d1b5c725d55392b951972d8313802bfe90ccac33f888ec34c63c
languageName: node
linkType: hard
"hast-util-from-html-isomorphic@npm:^2.0.0":
version: 2.0.0
resolution: "hast-util-from-html-isomorphic@npm:2.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
hast-util-from-dom: "npm:^5.0.0"
hast-util-from-html: "npm:^2.0.0"
unist-util-remove-position: "npm:^5.0.0"
checksum: 10c0/fc68d9245e794483a802d5c85a9f6c25959e00db78cc796411efc965134f3206f9cc9fa38134572ea781ad74663e801f1f83202007b208e27a770855566a62b6
languageName: node
linkType: hard
"hast-util-from-html@npm:^2.0.0":
version: 2.0.1
resolution: "hast-util-from-html@npm:2.0.1"
dependencies:
"@types/hast": "npm:^3.0.0"
devlop: "npm:^1.1.0"
hast-util-from-parse5: "npm:^8.0.0"
parse5: "npm:^7.0.0"
vfile: "npm:^6.0.0"
vfile-message: "npm:^4.0.0"
checksum: 10c0/856ceec209940ac4f9db52bf6338b97fb11f27e6d5b930f89676bc16ee282c06f9ff2a17254280803aefdf740507cf3004f181d0286b04dda11907852decbe77
languageName: node
linkType: hard
"hast-util-from-parse5@npm:^8.0.0":
version: 8.0.1
resolution: "hast-util-from-parse5@npm:8.0.1"
dependencies:
"@types/hast": "npm:^3.0.0"
"@types/unist": "npm:^3.0.0"
devlop: "npm:^1.0.0"
hastscript: "npm:^8.0.0"
property-information: "npm:^6.0.0"
vfile: "npm:^6.0.0"
vfile-location: "npm:^5.0.0"
web-namespaces: "npm:^2.0.0"
checksum: 10c0/4a30bb885cff1f0e023c429ae3ece73fe4b03386f07234bf23f5555ca087c2573ff4e551035b417ed7615bde559f394cdaf1db2b91c3b7f0575f3563cd238969
languageName: node
linkType: hard
"hast-util-is-element@npm:^3.0.0":
version: 3.0.0
resolution: "hast-util-is-element@npm:3.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
checksum: 10c0/f5361e4c9859c587ca8eb0d8343492f3077ccaa0f58a44cd09f35d5038f94d65152288dcd0c19336ef2c9491ec4d4e45fde2176b05293437021570aa0bc3613b
languageName: node
linkType: hard
"hast-util-parse-selector@npm:^2.0.0":
version: 2.2.5
resolution: "hast-util-parse-selector@npm:2.2.5"
@@ -5335,6 +5535,15 @@ __metadata:
languageName: node
linkType: hard
"hast-util-parse-selector@npm:^4.0.0":
version: 4.0.0
resolution: "hast-util-parse-selector@npm:4.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
checksum: 10c0/5e98168cb44470dc274aabf1a28317e4feb09b1eaf7a48bbaa8c1de1b43a89cd195cb1284e535698e658e3ec26ad91bc5e52c9563c36feb75abbc68aaf68fb9f
languageName: node
linkType: hard
"hast-util-to-jsx-runtime@npm:^2.0.0":
version: 2.3.0
resolution: "hast-util-to-jsx-runtime@npm:2.3.0"
@@ -5358,6 +5567,18 @@ __metadata:
languageName: node
linkType: hard
"hast-util-to-text@npm:^4.0.0":
version: 4.0.2
resolution: "hast-util-to-text@npm:4.0.2"
dependencies:
"@types/hast": "npm:^3.0.0"
"@types/unist": "npm:^3.0.0"
hast-util-is-element: "npm:^3.0.0"
unist-util-find-after: "npm:^5.0.0"
checksum: 10c0/93ecc10e68fe5391c6e634140eb330942e71dea2724c8e0c647c73ed74a8ec930a4b77043b5081284808c96f73f2bee64ee416038ece75a63a467e8d14f09946
languageName: node
linkType: hard
"hast-util-whitespace@npm:^3.0.0":
version: 3.0.0
resolution: "hast-util-whitespace@npm:3.0.0"
@@ -5380,6 +5601,19 @@ __metadata:
languageName: node
linkType: hard
"hastscript@npm:^8.0.0":
version: 8.0.0
resolution: "hastscript@npm:8.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
comma-separated-tokens: "npm:^2.0.0"
hast-util-parse-selector: "npm:^4.0.0"
property-information: "npm:^6.0.0"
space-separated-tokens: "npm:^2.0.0"
checksum: 10c0/f0b54bbdd710854b71c0f044612db0fe1b5e4d74fa2001633dc8c535c26033269f04f536f9fd5b03f234de1111808f9e230e9d19493bf919432bb24d541719e0
languageName: node
linkType: hard
"highlight.js@npm:^10.4.1, highlight.js@npm:~10.7.0":
version: 10.7.3
resolution: "highlight.js@npm:10.7.3"
@@ -5883,6 +6117,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 +6376,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"
@@ -6216,6 +6471,17 @@ __metadata:
languageName: node
linkType: hard
"katex@npm:^0.16.0":
version: 0.16.11
resolution: "katex@npm:0.16.11"
dependencies:
commander: "npm:^8.3.0"
bin:
katex: cli.js
checksum: 10c0/be405d45d7228bbfeecd491e0f74d9da0066b5e7b457e3f1dc833de5b63f9e98e40d2ef6b46e1cbe577490a43338c043851da032c45aeec0cc03ad431ef6fd83
languageName: node
linkType: hard
"keyv@npm:^4.0.0, keyv@npm:^4.5.3":
version: 4.5.4
resolution: "keyv@npm:4.5.4"
@@ -6269,6 +6535,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"
@@ -6402,6 +6678,13 @@ __metadata:
languageName: node
linkType: hard
"markdown-table@npm:^3.0.0":
version: 3.0.3
resolution: "markdown-table@npm:3.0.3"
checksum: 10c0/47433a3f31e4637a184e38e873ab1d2fadfb0106a683d466fec329e99a2d8dfa09f091fa42202c6f13ec94aef0199f449a684b28042c636f2edbc1b7e1811dcd
languageName: node
linkType: hard
"matcher@npm:^3.0.0":
version: 3.0.0
resolution: "matcher@npm:3.0.0"
@@ -6411,6 +6694,18 @@ __metadata:
languageName: node
linkType: hard
"mdast-util-find-and-replace@npm:^3.0.0":
version: 3.0.1
resolution: "mdast-util-find-and-replace@npm:3.0.1"
dependencies:
"@types/mdast": "npm:^4.0.0"
escape-string-regexp: "npm:^5.0.0"
unist-util-is: "npm:^6.0.0"
unist-util-visit-parents: "npm:^6.0.0"
checksum: 10c0/1faca98c4ee10a919f23b8cc6d818e5bb6953216a71dfd35f51066ed5d51ef86e5063b43dcfdc6061cd946e016a9f0d44a1dccadd58452cf4ed14e39377f00cb
languageName: node
linkType: hard
"mdast-util-from-markdown@npm:^2.0.0":
version: 2.0.1
resolution: "mdast-util-from-markdown@npm:2.0.1"
@@ -6431,6 +6726,98 @@ __metadata:
languageName: node
linkType: hard
"mdast-util-gfm-autolink-literal@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-autolink-literal@npm:2.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
ccount: "npm:^2.0.0"
devlop: "npm:^1.0.0"
mdast-util-find-and-replace: "npm:^3.0.0"
micromark-util-character: "npm:^2.0.0"
checksum: 10c0/821ef91db108f05b321c54fdf4436df9d6badb33e18f714d8d52c0e70f988f5b6b118cdd4d607b4cb3bef1718304ce7e9fb25fa580622c3d20d68c1489c64875
languageName: node
linkType: hard
"mdast-util-gfm-footnote@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-footnote@npm:2.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
devlop: "npm:^1.1.0"
mdast-util-from-markdown: "npm:^2.0.0"
mdast-util-to-markdown: "npm:^2.0.0"
micromark-util-normalize-identifier: "npm:^2.0.0"
checksum: 10c0/c673b22bea24740235e74cfd66765b41a2fa540334f7043fa934b94938b06b7d3c93f2d3b33671910c5492b922c0cc98be833be3b04cfed540e0679650a6d2de
languageName: node
linkType: hard
"mdast-util-gfm-strikethrough@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-strikethrough@npm:2.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
mdast-util-from-markdown: "npm:^2.0.0"
mdast-util-to-markdown: "npm:^2.0.0"
checksum: 10c0/b053e93d62c7545019bd914271ea9e5667ad3b3b57d16dbf68e56fea39a7e19b4a345e781312714eb3d43fdd069ff7ee22a3ca7f6149dfa774554f19ce3ac056
languageName: node
linkType: hard
"mdast-util-gfm-table@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-table@npm:2.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
devlop: "npm:^1.0.0"
markdown-table: "npm:^3.0.0"
mdast-util-from-markdown: "npm:^2.0.0"
mdast-util-to-markdown: "npm:^2.0.0"
checksum: 10c0/128af47c503a53bd1c79f20642561e54a510ad5e2db1e418d28fefaf1294ab839e6c838e341aef5d7e404f9170b9ca3d1d89605f234efafde93ee51174a6e31e
languageName: node
linkType: hard
"mdast-util-gfm-task-list-item@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-gfm-task-list-item@npm:2.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
devlop: "npm:^1.0.0"
mdast-util-from-markdown: "npm:^2.0.0"
mdast-util-to-markdown: "npm:^2.0.0"
checksum: 10c0/258d725288482b636c0a376c296431390c14b4f29588675297cb6580a8598ed311fc73ebc312acfca12cc8546f07a3a285a53a3b082712e2cbf5c190d677d834
languageName: node
linkType: hard
"mdast-util-gfm@npm:^3.0.0":
version: 3.0.0
resolution: "mdast-util-gfm@npm:3.0.0"
dependencies:
mdast-util-from-markdown: "npm:^2.0.0"
mdast-util-gfm-autolink-literal: "npm:^2.0.0"
mdast-util-gfm-footnote: "npm:^2.0.0"
mdast-util-gfm-strikethrough: "npm:^2.0.0"
mdast-util-gfm-table: "npm:^2.0.0"
mdast-util-gfm-task-list-item: "npm:^2.0.0"
mdast-util-to-markdown: "npm:^2.0.0"
checksum: 10c0/91596fe9bf3e4a0c546d0c57f88106c17956d9afbe88ceb08308e4da2388aff64489d649ddad599caecfdf755fc3ae4c9b82c219b85281bc0586b67599881fca
languageName: node
linkType: hard
"mdast-util-math@npm:^3.0.0":
version: 3.0.0
resolution: "mdast-util-math@npm:3.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
"@types/mdast": "npm:^4.0.0"
devlop: "npm:^1.0.0"
longest-streak: "npm:^3.0.0"
mdast-util-from-markdown: "npm:^2.0.0"
mdast-util-to-markdown: "npm:^2.1.0"
unist-util-remove-position: "npm:^5.0.0"
checksum: 10c0/d4e839e38719f26872ed78aac18339805a892f1b56585a9cb8668f34e221b4f0660b9dfe49ec96dbbe79fd1b63b648608a64046d8286bcd2f9d576e80b48a0a1
languageName: node
linkType: hard
"mdast-util-mdx-expression@npm:^2.0.0":
version: 2.0.0
resolution: "mdast-util-mdx-expression@npm:2.0.0"
@@ -6507,7 +6894,7 @@ __metadata:
languageName: node
linkType: hard
"mdast-util-to-markdown@npm:^2.0.0":
"mdast-util-to-markdown@npm:^2.0.0, mdast-util-to-markdown@npm:^2.1.0":
version: 2.1.0
resolution: "mdast-util-to-markdown@npm:2.1.0"
dependencies:
@@ -6570,6 +6957,114 @@ __metadata:
languageName: node
linkType: hard
"micromark-extension-gfm-autolink-literal@npm:^2.0.0":
version: 2.1.0
resolution: "micromark-extension-gfm-autolink-literal@npm:2.1.0"
dependencies:
micromark-util-character: "npm:^2.0.0"
micromark-util-sanitize-uri: "npm:^2.0.0"
micromark-util-symbol: "npm:^2.0.0"
micromark-util-types: "npm:^2.0.0"
checksum: 10c0/84e6fbb84ea7c161dfa179665dc90d51116de4c28f3e958260c0423e5a745372b7dcbc87d3cde98213b532e6812f847eef5ae561c9397d7f7da1e59872ef3efe
languageName: node
linkType: hard
"micromark-extension-gfm-footnote@npm:^2.0.0":
version: 2.1.0
resolution: "micromark-extension-gfm-footnote@npm:2.1.0"
dependencies:
devlop: "npm:^1.0.0"
micromark-core-commonmark: "npm:^2.0.0"
micromark-factory-space: "npm:^2.0.0"
micromark-util-character: "npm:^2.0.0"
micromark-util-normalize-identifier: "npm:^2.0.0"
micromark-util-sanitize-uri: "npm:^2.0.0"
micromark-util-symbol: "npm:^2.0.0"
micromark-util-types: "npm:^2.0.0"
checksum: 10c0/d172e4218968b7371b9321af5cde8c77423f73b233b2b0fcf3ff6fd6f61d2e0d52c49123a9b7910612478bf1f0d5e88c75a3990dd68f70f3933fe812b9f77edc
languageName: node
linkType: hard
"micromark-extension-gfm-strikethrough@npm:^2.0.0":
version: 2.1.0
resolution: "micromark-extension-gfm-strikethrough@npm:2.1.0"
dependencies:
devlop: "npm:^1.0.0"
micromark-util-chunked: "npm:^2.0.0"
micromark-util-classify-character: "npm:^2.0.0"
micromark-util-resolve-all: "npm:^2.0.0"
micromark-util-symbol: "npm:^2.0.0"
micromark-util-types: "npm:^2.0.0"
checksum: 10c0/ef4f248b865bdda71303b494671b7487808a340b25552b11ca6814dff3fcfaab9be8d294643060bbdb50f79313e4a686ab18b99cbe4d3ee8a4170fcd134234fb
languageName: node
linkType: hard
"micromark-extension-gfm-table@npm:^2.0.0":
version: 2.1.0
resolution: "micromark-extension-gfm-table@npm:2.1.0"
dependencies:
devlop: "npm:^1.0.0"
micromark-factory-space: "npm:^2.0.0"
micromark-util-character: "npm:^2.0.0"
micromark-util-symbol: "npm:^2.0.0"
micromark-util-types: "npm:^2.0.0"
checksum: 10c0/c1b564ab68576406046d825b9574f5b4dbedbb5c44bede49b5babc4db92f015d9057dd79d8e0530f2fecc8970a695c40ac2e5e1d4435ccf3ef161038d0d1463b
languageName: node
linkType: hard
"micromark-extension-gfm-tagfilter@npm:^2.0.0":
version: 2.0.0
resolution: "micromark-extension-gfm-tagfilter@npm:2.0.0"
dependencies:
micromark-util-types: "npm:^2.0.0"
checksum: 10c0/995558843fff137ae4e46aecb878d8a4691cdf23527dcf1e2f0157d66786be9f7bea0109c52a8ef70e68e3f930af811828ba912239438e31a9cfb9981f44d34d
languageName: node
linkType: hard
"micromark-extension-gfm-task-list-item@npm:^2.0.0":
version: 2.1.0
resolution: "micromark-extension-gfm-task-list-item@npm:2.1.0"
dependencies:
devlop: "npm:^1.0.0"
micromark-factory-space: "npm:^2.0.0"
micromark-util-character: "npm:^2.0.0"
micromark-util-symbol: "npm:^2.0.0"
micromark-util-types: "npm:^2.0.0"
checksum: 10c0/78aa537d929e9309f076ba41e5edc99f78d6decd754b6734519ccbbfca8abd52e1c62df68d41a6ae64d2a3fc1646cea955893c79680b0b4385ced4c52296181f
languageName: node
linkType: hard
"micromark-extension-gfm@npm:^3.0.0":
version: 3.0.0
resolution: "micromark-extension-gfm@npm:3.0.0"
dependencies:
micromark-extension-gfm-autolink-literal: "npm:^2.0.0"
micromark-extension-gfm-footnote: "npm:^2.0.0"
micromark-extension-gfm-strikethrough: "npm:^2.0.0"
micromark-extension-gfm-table: "npm:^2.0.0"
micromark-extension-gfm-tagfilter: "npm:^2.0.0"
micromark-extension-gfm-task-list-item: "npm:^2.0.0"
micromark-util-combine-extensions: "npm:^2.0.0"
micromark-util-types: "npm:^2.0.0"
checksum: 10c0/970e28df6ebdd7c7249f52a0dda56e0566fbfa9ae56c8eeeb2445d77b6b89d44096880cd57a1c01e7821b1f4e31009109fbaca4e89731bff7b83b8519690e5d9
languageName: node
linkType: hard
"micromark-extension-math@npm:^3.0.0":
version: 3.1.0
resolution: "micromark-extension-math@npm:3.1.0"
dependencies:
"@types/katex": "npm:^0.16.0"
devlop: "npm:^1.0.0"
katex: "npm:^0.16.0"
micromark-factory-space: "npm:^2.0.0"
micromark-util-character: "npm:^2.0.0"
micromark-util-symbol: "npm:^2.0.0"
micromark-util-types: "npm:^2.0.0"
checksum: 10c0/56e6f2185a4613f9d47e7e98cf8605851c990957d9229c942b005e286c8087b61dc9149448d38b2f8be6d42cc6a64aad7e1f2778ddd86fbbb1a2f48a3ca1872f
languageName: node
linkType: hard
"micromark-factory-destination@npm:^2.0.0":
version: 2.0.0
resolution: "micromark-factory-destination@npm:2.0.0"
@@ -6817,6 +7312,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 +7723,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 +7791,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 +7809,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 +7836,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 +7896,22 @@ __metadata:
languageName: node
linkType: hard
"parse5@npm:^7.0.0":
version: 7.1.2
resolution: "parse5@npm:7.1.2"
dependencies:
entities: "npm:^4.4.0"
checksum: 10c0/297d7af8224f4b5cb7f6617ecdae98eeaed7f8cbd78956c42785e230505d5a4f07cef352af10d3006fa5c1544b76b57784d3a22d861ae071bbc460c649482bf4
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 +8034,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 +8978,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"
@@ -8551,6 +9129,47 @@ __metadata:
languageName: node
linkType: hard
"rehype-katex@npm:^7.0.0":
version: 7.0.0
resolution: "rehype-katex@npm:7.0.0"
dependencies:
"@types/hast": "npm:^3.0.0"
"@types/katex": "npm:^0.16.0"
hast-util-from-html-isomorphic: "npm:^2.0.0"
hast-util-to-text: "npm:^4.0.0"
katex: "npm:^0.16.0"
unist-util-visit-parents: "npm:^6.0.0"
vfile: "npm:^6.0.0"
checksum: 10c0/4986d5db673576df0274464eafecef7c999fb72bf16e8df92454c68bf063b005010ab5465c64dacfbc1767ed6446dd03768917df7b9983f5e60711bce78b9880
languageName: node
linkType: hard
"remark-gfm@npm:^4.0.0":
version: 4.0.0
resolution: "remark-gfm@npm:4.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
mdast-util-gfm: "npm:^3.0.0"
micromark-extension-gfm: "npm:^3.0.0"
remark-parse: "npm:^11.0.0"
remark-stringify: "npm:^11.0.0"
unified: "npm:^11.0.0"
checksum: 10c0/db0aa85ab718d475c2596e27c95be9255d3b0fc730a4eda9af076b919f7dd812f7be3ac020611a8dbe5253fd29671d7b12750b56e529fdc32dfebad6dbf77403
languageName: node
linkType: hard
"remark-math@npm:^6.0.0":
version: 6.0.0
resolution: "remark-math@npm:6.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
mdast-util-math: "npm:^3.0.0"
micromark-extension-math: "npm:^3.0.0"
unified: "npm:^11.0.0"
checksum: 10c0/859613c4db194bb6b3c9c063661dc52b8ceda9c5cf3256b42f73d93eb8f38a6d634eb5f976fe094425f6f1035aaf329eb49ada314feb3b2b1073326b6d3aaa02
languageName: node
linkType: hard
"remark-parse@npm:^11.0.0":
version: 11.0.0
resolution: "remark-parse@npm:11.0.0"
@@ -8576,6 +9195,17 @@ __metadata:
languageName: node
linkType: hard
"remark-stringify@npm:^11.0.0":
version: 11.0.0
resolution: "remark-stringify@npm:11.0.0"
dependencies:
"@types/mdast": "npm:^4.0.0"
mdast-util-to-markdown: "npm:^2.0.0"
unified: "npm:^11.0.0"
checksum: 10c0/0cdb37ce1217578f6f847c7ec9f50cbab35df5b9e3903d543e74b405404e67c07defcb23cd260a567b41b769400f6de03c2c3d9cd6ae7a6707d5c8d89ead489f
languageName: node
linkType: hard
"require-directory@npm:^2.1.1":
version: 2.1.1
resolution: "require-directory@npm:2.1.1"
@@ -8583,6 +9213,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 +10128,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"
@@ -9615,6 +10259,16 @@ __metadata:
languageName: node
linkType: hard
"unist-util-find-after@npm:^5.0.0":
version: 5.0.0
resolution: "unist-util-find-after@npm:5.0.0"
dependencies:
"@types/unist": "npm:^3.0.0"
unist-util-is: "npm:^6.0.0"
checksum: 10c0/a7cea473c4384df8de867c456b797ff1221b20f822e1af673ff5812ed505358b36f47f3b084ac14c3622cb879ed833b71b288e8aa71025352a2aab4c2925a6eb
languageName: node
linkType: hard
"unist-util-is@npm:^6.0.0":
version: 6.0.0
resolution: "unist-util-is@npm:6.0.0"
@@ -9780,6 +10434,16 @@ __metadata:
languageName: node
linkType: hard
"vfile-location@npm:^5.0.0":
version: 5.0.3
resolution: "vfile-location@npm:5.0.3"
dependencies:
"@types/unist": "npm:^3.0.0"
vfile: "npm:^6.0.0"
checksum: 10c0/1711f67802a5bc175ea69750d59863343ed43d1b1bb25c0a9063e4c70595e673e53e2ed5cdbb6dcdc370059b31605144d95e8c061b9361bcc2b036b8f63a4966
languageName: node
linkType: hard
"vfile-message@npm:^4.0.0":
version: 4.0.2
resolution: "vfile-message@npm:4.0.2"
@@ -9848,6 +10512,13 @@ __metadata:
languageName: node
linkType: hard
"web-namespaces@npm:^2.0.0":
version: 2.0.1
resolution: "web-namespaces@npm:2.0.1"
checksum: 10c0/df245f466ad83bd5cd80bfffc1674c7f64b7b84d1de0e4d2c0934fb0782e0a599164e7197a4bce310ee3342fd61817b8047ff04f076a1ce12dd470584142a4bd
languageName: node
linkType: hard
"web-streams-polyfill@npm:4.0.0-beta.3":
version: 4.0.0-beta.3
resolution: "web-streams-polyfill@npm:4.0.0-beta.3"