Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
297539bab7 | ||
|
|
911c2d0202 | ||
|
|
2969a05f10 | ||
|
|
5d90489a04 | ||
|
|
18fa1c92a4 | ||
|
|
937e62bf9d | ||
|
|
6291a463d8 | ||
|
|
681c93f5eb | ||
|
|
23687f119d | ||
|
|
0bcdffc159 | ||
|
|
b04b0cc8a6 | ||
|
|
c9a964d8f8 | ||
|
|
86fc4676ba | ||
|
|
527afa1357 | ||
|
|
384178c617 | ||
|
|
c53e35db76 | ||
|
|
c36075f0b5 | ||
|
|
5c95373a37 | ||
|
|
29d6d607da | ||
|
|
e64375a74c | ||
|
|
4689bb53e9 | ||
|
|
e00c66e54a | ||
|
|
62b0908dfa | ||
|
|
cb0b9de1e9 | ||
|
|
d8d4afbc0d | ||
|
|
c50ff4585a | ||
|
|
a5ee8548f3 | ||
|
|
15b286a095 | ||
|
|
d47d4a158d | ||
|
|
cd85dcddf8 | ||
|
|
925a9fb8ec | ||
|
|
17c3437e02 | ||
|
|
69293846fc | ||
|
|
20a7fbfc48 | ||
|
|
64d4b8450a | ||
|
|
f080fc5048 | ||
|
|
50f08124d7 | ||
|
|
b91081ef99 | ||
|
|
d869ec9a9b | ||
|
|
70c4354d6c | ||
|
|
527c4e77dc | ||
|
|
2483ce3bb4 | ||
|
|
db3f8b8bee | ||
|
|
45bf3d4e86 | ||
|
|
59b39dc41a | ||
|
|
a267a8d4c3 | ||
|
|
5b123f2c33 | ||
|
|
fe34fb3c25 | ||
|
|
e6359d2048 |
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
114
LICENSE
@@ -1,21 +1,101 @@
|
||||
MIT License
|
||||
### Cherry Studio 商业许可协议
|
||||
|
||||
Copyright (c) 2024 亢奋猫
|
||||
---
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
#### 中文版
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
**Cherry Studio 商业许可协议**
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
本协议(以下简称“协议”)由以下双方签订:
|
||||
|
||||
- 许可方:王谦(kangfenmao@qq.com)
|
||||
- 被许可方:[被许可方名称]
|
||||
|
||||
**1. 定义**
|
||||
|
||||
- “软件”指 Cherry Studio 软件,网址为 https://cherry-ai.com。
|
||||
- “商业用途”指任何以盈利为目的的使用。
|
||||
|
||||
**2. 许可**
|
||||
|
||||
- 未经许可方明确书面许可,被许可方不得将软件用于商业用途。
|
||||
- 未经许可方事先书面同意,被许可方不得将软件全部或部分用于商业用途分发。
|
||||
- 未经许可方明确授权,被许可方不得再许可、租赁、销售、出租或以其他方式将软件转让给任何第三方用于商业用途。
|
||||
|
||||
**3. 责任限制**
|
||||
|
||||
开发者不对因使用本软件而产生的任何直接或间接损失承担责任。用户应自行承担使用本软件的风险。
|
||||
|
||||
**4. 许可协议生效日期**
|
||||
|
||||
本许可协议自用户首次下载或使用本软件之日起生效。
|
||||
|
||||
**5. 许可终止**
|
||||
|
||||
如发现用户违反上述条款,开发者有权随时终止本许可,并要求用户停止使用本软件及删除所有相关副本。
|
||||
|
||||
**6. 其他**
|
||||
|
||||
本协议的解释、效力及争议的解决,均适用中华人民共和国法律。
|
||||
|
||||
**7. 联系信息**
|
||||
|
||||
- 许可方联系方式:
|
||||
- 手机号:18539907620
|
||||
- 邮箱:kangfenmao@qq.com
|
||||
|
||||
**许可方(签字):**
|
||||
**日期:**
|
||||
|
||||
**被许可方(签字):**
|
||||
**日期:**
|
||||
|
||||
---
|
||||
|
||||
#### English Version
|
||||
|
||||
**Cherry Studio Commercial License Agreement**
|
||||
|
||||
This Agreement ("Agreement") is entered into by and between:
|
||||
|
||||
- Licensor: Wang Qian (kangfenmao)
|
||||
- Licensee: [Licensee Name]
|
||||
|
||||
**1. Definitions**
|
||||
|
||||
- "Software" refers to the Cherry Studio software, available at https://cherry-ai.com.
|
||||
- "Commercial Use" refers to any use for profit.
|
||||
|
||||
**2. License**
|
||||
|
||||
- The Licensee may not use the Software for Commercial Use without the Licensor's explicit written permission.
|
||||
- The Licensee may not distribute the Software in whole or in part for Commercial Use without the Licensor's prior written consent.
|
||||
- The Licensee may not sublicense, lease, sell, rent, or otherwise transfer the Software to any third party for Commercial Use without the Licensor's explicit authorization.
|
||||
|
||||
**3. Termination of License**
|
||||
|
||||
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
|
||||
|
||||
**4. Effective Date of License Agreement**
|
||||
|
||||
This license agreement becomes effective from the date the user first downloads or uses the software.
|
||||
|
||||
**5. Termination of License**
|
||||
|
||||
The developer reserves the right to terminate this license at any time if the terms are violated, and may require the user to cease using the software and delete all related copies.
|
||||
|
||||
**6. Miscellaneous**
|
||||
|
||||
This Agreement shall be governed by and construed in accordance with the laws of the People's Republic of China.
|
||||
|
||||
**7. Contact Information**
|
||||
|
||||
- Licensor's Contact Details:
|
||||
- Phone: 18539907620
|
||||
- Email: kangfenmao@qq.com
|
||||
|
||||
**Licensor (Signature):**
|
||||
**Date:**
|
||||
|
||||
**Licensee (Signature):**
|
||||
**Date:**
|
||||
|
||||
@@ -56,8 +56,5 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
1. 保存聊天页面状态,切换页面后恢复
|
||||
2. 修复默认助手名称为空时候的显示问题
|
||||
3. 简化系统内置智能体提示词长度
|
||||
4. 增加单个聊天内容保存到本地功能
|
||||
5. 系统内置提供商支持修改 API 地址
|
||||
新增发送按钮
|
||||
输入区域展开可以全屏显示
|
||||
|
||||
@@ -1,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({
|
||||
|
||||
13
package.json
13
package.json
@@ -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
15
src/main/config.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Store from 'electron-store'
|
||||
|
||||
export const appConfig = new Store()
|
||||
|
||||
export const titleBarOverlayDark = {
|
||||
height: 41,
|
||||
color: '#00000000',
|
||||
symbolColor: '#ffffff'
|
||||
}
|
||||
|
||||
export const titleBarOverlayLight = {
|
||||
height: 41,
|
||||
color: '#00000000',
|
||||
symbolColor: '#000000'
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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('触发检查更新')
|
||||
|
||||
@@ -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
|
||||
|
||||
1
src/preload/index.d.ts
vendored
1
src/preload/index.d.ts
vendored
@@ -12,6 +12,7 @@ declare global {
|
||||
openWebsite: (url: string) => void
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
saveFile: (path: string, content: string) => void
|
||||
setTheme: (theme: 'light' | 'dark') => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<title>Cherry Studio</title>
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' *; img-src 'self' data:" />
|
||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data:" />
|
||||
</head>
|
||||
|
||||
<body theme-mode="dark">
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
@@ -1,33 +1,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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Black.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Black.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Bold.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Bold.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-ExtraBold.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-ExtraLight.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Light.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Light.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Medium.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Medium.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Regular.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Regular.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-SemiBold.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Thin.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Poppins/Poppins-Thin.ttf
Normal file
Binary file not shown.
47
src/renderer/src/assets/fonts/Poppins/Poppins.css
Normal file
47
src/renderer/src/assets/fonts/Poppins/Poppins.css
Normal 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;
|
||||
}
|
||||
55
src/renderer/src/assets/fonts/icon-fonts/iconfont.css
Normal file
55
src/renderer/src/assets/fonts/icon-fonts/iconfont.css
Normal file
@@ -0,0 +1,55 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1722242729348') format('woff2'),
|
||||
url('iconfont.woff?t=1722242729348') format('woff'),
|
||||
url('iconfont.ttf?t=1722242729348') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-dark1:before {
|
||||
content: "\e72f";
|
||||
}
|
||||
|
||||
.icon-theme-light:before {
|
||||
content: "\e6b7";
|
||||
}
|
||||
|
||||
.icon-translate_line:before {
|
||||
content: "\e7de";
|
||||
}
|
||||
|
||||
.icon-history:before {
|
||||
content: "\e758";
|
||||
}
|
||||
|
||||
.icon-hidesidebarhoriz:before {
|
||||
content: "\e8eb";
|
||||
}
|
||||
|
||||
.icon-showsidebarhoriz:before {
|
||||
content: "\e944";
|
||||
}
|
||||
|
||||
.icon-a-addchat:before {
|
||||
content: "\e658";
|
||||
}
|
||||
|
||||
.icon-appstore:before {
|
||||
content: "\e792";
|
||||
}
|
||||
|
||||
.icon-chat:before {
|
||||
content: "\e615";
|
||||
}
|
||||
|
||||
.icon-setting:before {
|
||||
content: "\e78e";
|
||||
}
|
||||
|
||||
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.ttf
Normal file
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff
Normal file
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2
Normal file
BIN
src/renderer/src/assets/fonts/icon-fonts/iconfont.woff2
Normal file
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 7.9 KiB |
@@ -1,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
{
|
||||
"id": 2,
|
||||
"name": "🎯 策略产品经理 - Strategy Product Manager",
|
||||
"emoji": "🎯 ",
|
||||
"emoji": "🎯",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。"
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
2
src/renderer/src/env.d.ts
vendored
2
src/renderer/src/env.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }))
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
119
src/renderer/src/pages/home/components/Suggestions.tsx
Normal file
119
src/renderer/src/pages/home/components/Suggestions.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
8
src/renderer/src/pages/home/components/markdown/Link.tsx
Normal file
8
src/renderer/src/pages/home/components/markdown/Link.tsx
Normal 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
|
||||
49
src/renderer/src/pages/home/components/markdown/Markdown.tsx
Normal file
49
src/renderer/src/pages/home/components/markdown/Markdown.tsx
Normal 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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
`
|
||||
|
||||
159
src/renderer/src/pages/home/components/sidebar/TopicsTab.tsx
Normal file
159
src/renderer/src/pages/home/components/sidebar/TopicsTab.tsx
Normal 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
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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;
|
||||
`
|
||||
|
||||
|
||||
307
src/renderer/src/pages/translate/TranslatePage.tsx
Normal file
307
src/renderer/src/pages/translate/TranslatePage.tsx
Normal 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
|
||||
38
src/renderer/src/providers/AntdProvider.tsx
Normal file
38
src/renderer/src/providers/AntdProvider.tsx
Normal 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
|
||||
43
src/renderer/src/providers/ThemeProvider.tsx
Normal file
43
src/renderer/src/providers/ThemeProvider.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { ThemeMode } from '@renderer/store/settings'
|
||||
import React, { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: ThemeMode
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType>({
|
||||
theme: ThemeMode.light,
|
||||
toggleTheme: () => {}
|
||||
})
|
||||
|
||||
export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { theme, setTheme } = useSettings()
|
||||
const [_theme, _setTheme] = useState(theme)
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === ThemeMode.dark ? ThemeMode.light : ThemeMode.dark)
|
||||
}
|
||||
|
||||
useEffect((): any => {
|
||||
if (theme === ThemeMode.auto) {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
_setTheme(mediaQuery.matches ? ThemeMode.dark : ThemeMode.light)
|
||||
const handleChange = (e: MediaQueryListEvent) => _setTheme(e.matches ? ThemeMode.dark : ThemeMode.light)
|
||||
mediaQuery.addEventListener('change', handleChange)
|
||||
return () => mediaQuery.removeEventListener('change', handleChange)
|
||||
} else {
|
||||
_setTheme(theme)
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('theme-mode', _theme)
|
||||
window.api?.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light')
|
||||
}, [_theme])
|
||||
|
||||
return <ThemeContext.Provider value={{ theme: _theme, toggleTheme }}>{children}</ThemeContext.Provider>
|
||||
}
|
||||
|
||||
export const useTheme = () => useContext(ThemeContext)
|
||||
@@ -1,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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 : []
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,12 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
|
||||
|
||||
export type SendMessageShortcut = 'Enter' | 'Shift+Enter'
|
||||
|
||||
export enum ThemeMode {
|
||||
light = 'light',
|
||||
dark = 'dark',
|
||||
auto = 'auto'
|
||||
}
|
||||
|
||||
export interface SettingsState {
|
||||
showRightSidebar: boolean
|
||||
showAssistants: boolean
|
||||
@@ -12,6 +18,7 @@ export interface SettingsState {
|
||||
showMessageDivider: boolean
|
||||
messageFont: 'system' | 'serif'
|
||||
showInputEstimatedTokens: boolean
|
||||
theme: ThemeMode
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
@@ -23,7 +30,8 @@ const initialState: SettingsState = {
|
||||
userName: '',
|
||||
showMessageDivider: true,
|
||||
messageFont: 'system',
|
||||
showInputEstimatedTokens: false
|
||||
showInputEstimatedTokens: false,
|
||||
theme: ThemeMode.light
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
@@ -59,6 +67,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setShowInputEstimatedTokens: (state, action: PayloadAction<boolean>) => {
|
||||
state.showInputEstimatedTokens = action.payload
|
||||
},
|
||||
setTheme: (state, action: PayloadAction<ThemeMode>) => {
|
||||
state.theme = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -73,7 +84,8 @@ export const {
|
||||
setUserName,
|
||||
setShowMessageDivider,
|
||||
setMessageFont,
|
||||
setShowInputEstimatedTokens
|
||||
setShowInputEstimatedTokens,
|
||||
setTheme
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@@ -55,6 +55,7 @@ export type Model = {
|
||||
provider: string
|
||||
name: string
|
||||
group: string
|
||||
owned_by?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
@@ -66,3 +67,7 @@ export type SystemAssistant = {
|
||||
prompt: string
|
||||
group: string
|
||||
}
|
||||
|
||||
export type Suggestion = {
|
||||
content: string
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
675
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user