Compare commits

...

63 Commits

Author SHA1 Message Date
kangfenmao
aa3b2d6290 chore(version): 0.3.7 2024-07-26 11:04:02 +08:00
kangfenmao
c0e51c3992 feat(ProviderSetting.tsx): add remove icon for models to allow deletion 2024-07-26 10:40:53 +08:00
kangfenmao
8c80cc00b3 feat(provider.ts): add API endpoint configuration for each provider with editable status 2024-07-26 10:34:55 +08:00
kangfenmao
f961accd86 fix(SettingsTab.tsx): reposition reset button to align with model settings title for better visibility 2024-07-26 10:10:34 +08:00
kangfenmao
7de91d236d feat(NavigationCenter.tsx): add CodeSandboxOutlined icon to model selection 2024-07-26 10:04:59 +08:00
kangfenmao
2fdf0acec6 feat: add global _activeAssistant and_activeTopic variable to persist state across re-renders 2024-07-26 09:57:49 +08:00
kangfenmao
40e76f3e53 feat: save file to disk 2024-07-26 09:53:07 +08:00
kangfenmao
d7b8721848 refactor: remove conditional devTools enabling 2024-07-25 18:04:12 +08:00
kangfenmao
b91b0dd8e4 fix(api.ts): add null return if provider apiKey is missing to prevent unauthorized requests 2024-07-25 18:00:32 +08:00
kangfenmao
bb9b053924 docs(assistants.json): simplify prompts for clarity and consistency #6 2024-07-25 17:50:26 +08:00
kangfenmao
5743046200 refactor: use —narbar-background 2024-07-25 15:55:23 +08:00
kangfenmao
a507776c1e fix: default assistant name is empty 2024-07-25 14:03:54 +08:00
kangfenmao
e74c828379 feat: set provider as default setting entry 2024-07-25 13:45:43 +08:00
kangfenmao
4b264c6a6b 0.3.6 2024-07-24 19:19:35 +08:00
kangfenmao
d21a4dce92 feat(ui): optimize messages ui styles 2024-07-24 19:17:58 +08:00
kangfenmao
74df29604b chore(version): v0.3.5 2024-07-24 18:37:48 +08:00
kangfenmao
8807783aa6 feat: switch topic tab on change assistant 2024-07-24 18:28:23 +08:00
kangfenmao
f81b38a362 perf(mermaid): lazy load mermaid 2024-07-24 18:19:43 +08:00
kangfenmao
d0280186bc feat: add setting panel 2024-07-24 18:08:05 +08:00
kangfenmao
9d96b826e2 feat(settings): add input status show switch 2024-07-24 13:08:30 +08:00
kangfenmao
ec20750e64 fix: sidebar mac style 2024-07-24 12:28:56 +08:00
kangfenmao
51f4653cde feat(settings): add messageFont setting 2024-07-24 12:25:36 +08:00
kangfenmao
3625eefec4 fix: prevent navigate to new url 2024-07-23 19:08:36 +08:00
亢奋猫
1e1414d659 Update README.md 2024-07-23 18:39:06 +08:00
亢奋猫
b7162663f2 Update README.md 2024-07-23 18:24:12 +08:00
kangfenmao
1dd1bb5804 0.3.4 2024-07-23 18:10:33 +08:00
kangfenmao
4dd6c46035 fix: message style 2024-07-23 18:10:25 +08:00
kangfenmao
4036c36753 feat: add Mermaid render 2024-07-23 18:05:14 +08:00
kangfenmao
764aadd234 feat: change message font 2024-07-23 17:42:52 +08:00
kangfenmao
3d801f1552 feat: optimize message style 2024-07-23 17:32:06 +08:00
kangfenmao
bd865f0270 fix: windows title style 2024-07-23 16:55:32 +08:00
kangfenmao
93505a4bc6 feat: hide window title 2024-07-23 16:40:06 +08:00
kangfenmao
c43be11d20 feat: add username and message divider line settings 2024-07-23 15:16:34 +08:00
kangfenmao
8535edbdd1 feat: messages styles optimization 2024-07-23 14:59:09 +08:00
kangfenmao
731fb7860b 0.3.3 2024-07-23 12:37:40 +08:00
kangfenmao
4a32976483 fix: proxy check 2024-07-23 12:37:12 +08:00
kangfenmao
dedabe320e feat: new navbar style 2024-07-23 12:29:20 +08:00
kangfenmao
235b481645 feat: change icons 2024-07-23 10:42:58 +08:00
kangfenmao
58c5ace678 fix: inputbar setShowRightSidebar 2024-07-23 10:20:57 +08:00
kangfenmao
973d24271b feat(settings): add proxy setting 2024-07-23 00:28:41 +08:00
kangfenmao
f434fe1231 feat: add show or hide assistant sidebar 2024-07-22 21:57:39 +08:00
kangfenmao
a0c147ae3f feat(website): fetch github release info 2024-07-22 15:40:30 +08:00
kangfenmao
8d7cde1231 0.3.2 2024-07-22 14:52:54 +08:00
kangfenmao
87c04408de feat: add contextCount to inputbar 2024-07-22 14:50:40 +08:00
kangfenmao
2592448c74 feat: add email to about titles 2024-07-22 14:26:35 +08:00
kangfenmao
6f054874e8 chore: remove change log component 2024-07-22 14:25:15 +08:00
kangfenmao
40d687104e feat: new about page 2024-07-22 14:24:14 +08:00
kangfenmao
ac3cfe2878 fix: disable switch while assistant generating message 2024-07-22 11:28:26 +08:00
kangfenmao
e9a7735fce feat: add updateAssistantSettings to useAssistant hook 2024-07-22 11:15:10 +08:00
kangfenmao
c1a8198575 fix(ProviderSDK): clarify instruction for session summary to avoid punctuation marks and special characters 2024-07-22 10:49:10 +08:00
kangfenmao
8b45548b79 refactor: topic component code 2024-07-22 10:38:00 +08:00
kangfenmao
3f3b930819 fix: disabled switch topic while generating message 2024-07-22 10:22:47 +08:00
kangfenmao
a5d6e2c5c5 0.3.1 2024-07-21 23:44:09 +08:00
kangfenmao
2993ab8dc1 fix: topic missing bug and delete assistant crash 2024-07-21 23:43:17 +08:00
kangfenmao
117069e450 chore(version): 0.3.0 2024-07-21 22:03:49 +08:00
kangfenmao
c5965dc696 fix: assistant settings bugs 2024-07-21 21:57:08 +08:00
kangfenmao
4169a2ef35 feat: add asistant model temperature maxTokens contextCount 2024-07-21 17:50:50 +08:00
kangfenmao
75c37632d4 feat: change default assistant name
# Conflicts:
#	src/renderer/src/i18n/index.ts
2024-07-21 10:51:33 +08:00
亢奋猫
3f5c151a11 Update README.md 2024-07-20 15:10:49 +08:00
kangfenmao
d049e36c46 0.2.9 2024-07-20 12:47:29 +08:00
kangfenmao
d05fc1c9be chore(version): v0.2.9 2024-07-20 12:47:19 +08:00
kangfenmao
f33317a3fb fix: send message setting position 2024-07-20 11:34:52 +08:00
kangfenmao
f2b5ed09c0 feat(provider): add AiHubMix provider 2024-07-20 11:29:24 +08:00
70 changed files with 1924 additions and 868 deletions

3
.gitignore vendored
View File

@@ -34,6 +34,9 @@ npm/*/*
!.yarn/sdks
!.yarn/versions
# Windows
Thumbs.db
# Project
node_modules
dist

View File

@@ -4,11 +4,11 @@
# Screenshot
![image](https://github.com/user-attachments/assets/1763dc38-bece-4d24-9c21-ed82f6142694)
![](https://github.com/user-attachments/assets/e32b244f-3a84-473a-89ef-0b12ef4127b2)
![](https://github.com/user-attachments/assets/18c10eed-4711-4975-bf9c-b274c61924f3)
![image.png](https://s2.loli.net/2024/07/16/IQPz12OajfNoBTV.png)
![](https://github.com/user-attachments/assets/7395ebf2-64f8-46fa-aa48-63293516c320)
# Feature
@@ -18,6 +18,7 @@
4. Allows using multiple models to answer questions in the same conversation.
5. Supports drag-and-drop sorting.
6. Code highlighting.
7. Mermaid chart
# Develop
## Recommended IDE Setup

View File

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

View File

@@ -16,9 +16,6 @@ export default defineConfig({
}
},
plugins: [react()],
assetsInclude: ['**/*.md'],
server: {
host: '0.0.0.0'
}
assetsInclude: ['**/*.md']
}
})

View File

@@ -1,6 +1,6 @@
{
"name": "cherry-studio",
"version": "0.2.8",
"version": "0.3.7",
"description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js",
"author": "kangfenmao@qq.com",
@@ -18,7 +18,8 @@
"build:unpack": "dotenv npm run build && electron-builder --dir",
"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"
"build:linux": "dotenv electron-vite build && electron-builder --linux --publish never",
"release": "node scripts/version.js"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
@@ -33,7 +34,6 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^1.0.1",
"@electron-toolkit/tsconfig": "^1.0.1",
"@fontsource/inter": "^5.0.18",
"@hello-pangea/dnd": "^16.6.0",
"@kangfenmao/keyv-storage": "^0.1.0",
"@reduxjs/toolkit": "^2.2.5",
@@ -56,6 +56,7 @@
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.0.0",
"gpt-tokens": "^1.3.6",
"i18next": "^23.11.5",
"localforage": "^1.10.0",
"lodash": "^4.17.21",

40
scripts/version.js Normal file
View File

@@ -0,0 +1,40 @@
const { execSync } = require('child_process')
const fs = require('fs')
// 执行命令并返回输出
function exec(command) {
return execSync(command, { encoding: 'utf8' }).trim()
}
// 获取命令行参数
const args = process.argv.slice(2)
const versionType = args[0] || 'patch'
const shouldPush = args.includes('push')
// 验证版本类型
if (!['patch', 'minor', 'major'].includes(versionType)) {
console.error('Invalid version type. Use patch, minor, or major.')
process.exit(1)
}
// 更新版本
exec(`yarn version ${versionType} --immediate`)
// 读取更新后的 package.json 获取新版本号
const updatedPackageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'))
const newVersion = updatedPackageJson.version
// Git 操作
exec('git add .')
exec(`git commit -m "chore(version): ${newVersion}"`)
exec(`git tag -a v${newVersion} -m "Version ${newVersion}"`)
console.log(`Version bumped to ${newVersion}`)
if (shouldPush) {
console.log('Pushing to remote...')
exec('git push && git push --tags')
console.log('Pushed to remote.')
} else {
console.log('Changes are committed locally. Use "git push && git push --tags" to push to remote.')
}

24
src/main/event.ts Normal file
View File

@@ -0,0 +1,24 @@
import { dialog, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
import { writeFile } from 'fs'
import logger from 'electron-log'
export async function saveFile(_: Electron.IpcMainInvokeEvent, fileName: string, content: string): Promise<void> {
try {
const options: SaveDialogOptions = {
title: '保存文件',
defaultPath: fileName
}
const result: SaveDialogReturnValue = await dialog.showSaveDialog(options)
if (!result.canceled && result.filePath) {
writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => {
if (err) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
}
})
}
} catch (err) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
}
}

View File

@@ -1,11 +1,12 @@
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import * as Sentry from '@sentry/electron/main'
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, shell } from 'electron'
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 { saveFile } from './event'
function createWindow() {
// Load the previous state with fallback to defaults
@@ -24,14 +25,19 @@ function createWindow() {
minHeight: 500,
show: true,
autoHideMenuBar: true,
titleBarStyle: 'hiddenInset',
titleBarStyle: 'hidden',
titleBarOverlay: {
height: 41,
color: '#1f1f1f',
symbolColor: '#eee'
},
trafficLightPosition: { x: 8, y: 12 },
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
devTools: !app.isPackaged,
webSecurity: false
// devTools: !app.isPackaged,
}
})
@@ -47,6 +53,11 @@ function createWindow() {
menu.popup()
})
mainWindow.webContents.on('will-navigate', (event, url) => {
event.preventDefault()
shell.openExternal(url)
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
@@ -101,6 +112,12 @@ app.whenReady().then(() => {
shell.openExternal(url)
})
ipcMain.handle('set-proxy', (_, proxy: string) => {
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
})
ipcMain.handle('save-file', saveFile)
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.handle('check-for-update', async () => {
autoUpdater.logger?.info('触发检查更新')

View File

@@ -10,6 +10,8 @@ declare global {
}>
checkForUpdate: () => void
openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void
saveFile: (path: string, content: string) => void
}
}
}

View File

@@ -5,7 +5,9 @@ import { electronAPI } from '@electron-toolkit/preload'
const api = {
getAppInfo: () => ipcRenderer.invoke('get-app-info'),
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url)
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)
}
// Use `contextBridge` APIs to expose Electron APIs to

View File

@@ -7,7 +7,7 @@
<!-- 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' *; img-src 'self' data:" />
</head>
<body theme-mode="dark">

View File

@@ -1,4 +1,3 @@
import '@fontsource/inter'
import store, { persistor } from '@renderer/store'
import { ConfigProvider } from 'antd'
import { Provider } from 'react-redux'

View File

@@ -1,45 +0,0 @@
# CHANGES LOG
### v0.2.8 - 2024-07-20
- 🆕 Feature: Add customized service providers
### v0.2.7 - 2024-07-19
- 📢 Add DashScope Provider
- 📢 Add Anthropic Provider
### v0.2.6 - 2024-07-17
- 🆕 Fixed the issue of the BaiChuan API KEY not displaying when clicking to obtain the URL
- 📢 New intelligent body center style
### v0.2.5 - 2024-07-17
- 🆕 Baichuan AI Service Providers
- 📢 New Intelligent Agent Page with Multiple Professional Assistants
- 🌐 Multilingual Issue Fixes and Detailed Optimizations
### v0.2.4 - 2024-07-16
- Fixed the issue of the update log page cannot be scrolled
- Added a check for updates button
### v0.2.3 - 2024-07-16
- Fixed multi-language prompt errors
- Fixed default model error issues with ZHIPU AI
- Fixed OpenRouter API detection error issues
- Fixed multi-language translation errors with model providers
### v0.2.2 - 2024-07-15
- Fix the issue where the default assistant name is empty.
- Fix the problem with default language detection during the first installation.
- Adjust the changelog style.
### v0.2.1 - 2024-07-15
- **Feature**: Add new feature for pausing message sending
- **Fix**: Resolve incomplete translation issue upon language switch
- **Build**: Support for macOS Intel architecture

View File

@@ -1,46 +0,0 @@
# 更新日志
### v0.2.8 - 2024-07-20
- 🆕 新功能: 可以添加自定义服务提供商了
### v0.2.7 - 2024-07-19
- 📢 新增阿里云灵积服务商
- 📢 新增 Anthropic 服务商
### v0.2.6 - 2024-07-17
- 🆕 修复百川 API KEY 点击获取网址没有显示问题
- 📢 新的智能体中心样式
### v0.2.5 - 2024-07-17
- 🆕 新增百川AI服务商
- 📢 全新的智能体页面,新增多种职业助手
- 🌐 多语言问题修复,细节优化
### v0.2.4 - 2024-07-16
- 修复更新日志页面不能滚动问题
- 新增检查更新按钮
### v0.2.3 - 2024-07-16
- 修复多语言提示错误
- 修复智谱AI默认模型错误问题
- 修复 OpenRouter API 检测出错问题
- 修复模型提供商多语言翻译错误问题
### v0.2.2 - 2024-07-15
- 修复默认助理名称为空的问题
- 修复首次安装默认语言检测问题
- 更新日志样式微调
### v0.2.1 - 2024-07-15
- 【功能】新增消息暂停发送功能
- 【修复】修复多语言切换不彻底问题
- 【构建】支持 macOS Intel 架构

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -1,5 +1,6 @@
@import 'https://at.alicdn.com/t/c/font_4563475_hrx8c92awui.css';
@import './markdown.scss';
@import './scrollbar.scss';
// @font-face {
// font-family: 'Playwrite';
@@ -12,7 +13,7 @@
--color-white-mute: #f2f2f2;
--color-black: #1b1b1f;
--color-black-soft: #303030;
--color-black-soft: #262626;
--color-black-mute: #363636;
--color-gray-1: #515c67;
@@ -27,18 +28,24 @@
--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-text: var(--color-text-1);
--color-icon: #ffffff99;
--color-icon-white: #ffffff;
--color-border: #ffffff20;
--color-error: #f44336;
--navbar-background: #1f1f1f;
--navbar-height: 42px;
--sidebar-width: 55px;
--assistants-width: 235px;
--topic-list-width: 250px;
--assistants-width: 245px;
--topic-list-width: 260px;
--settings-width: var(--assistants-width);
--status-bar-height: 40px;
--input-bar-height: 120px;
--input-bar-height: 125px;
}
*,
@@ -61,19 +68,8 @@ body {
line-height: 1.6;
overflow: hidden;
background-size: cover;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-family: -apple-system, BlinkMacSystemFont, '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;
@@ -101,30 +97,3 @@ body,
#inputbar .ant-input {
resize: none;
}
/* 全局初始化滚动条样式 */
::-webkit-scrollbar {
width: 5px;
height: 5px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
/* Safari 和 Chrome */
@media screen and (-webkit-min-device-pixel-ratio: 0) {
body {
scrollbar-width: thin; /* 告诉 FF 用细滚动条 */
scrollbar-color: rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.1); /* FF 前面色后面色 */
}
}

View File

@@ -1,9 +1,12 @@
.markdown {
color: #fff;
color: #f1f1f1;
font-size: 15px;
line-height: 1.6;
user-select: text;
margin-top: 4px;
p:last-child {
margin-bottom: 5px;
}
p:first-of-type {
margin-top: 0;

View File

@@ -0,0 +1,15 @@
/* 全局初始化滚动条样式 */
::-webkit-scrollbar {
width: 3px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
&:hover {
background: rgba(255, 255, 255, 0.3);
}
}

View File

@@ -1,4 +1,4 @@
import { useAppInitEffect } from '@renderer/hooks/useAppInitEffect'
import { useAppInit } from '@renderer/hooks/useAppInit'
import { message, Modal } from 'antd'
import { findIndex, pullAt } from 'lodash'
import React, { useEffect, useState } from 'react'
@@ -29,7 +29,7 @@ const TopViewContainer: React.FC<Props> = ({ children }) => {
const [messageApi, messageContextHolder] = message.useMessage()
const [modal, modalContextHolder] = Modal.useModal()
useAppInitEffect()
useAppInit()
onPop = () => {
const views = [...elements]

View File

@@ -25,16 +25,15 @@ const NavbarContainer = styled.div`
flex-direction: row;
min-height: var(--navbar-height);
max-height: var(--navbar-height);
border-bottom: 0.5px solid var(--color-border);
-webkit-app-region: drag;
background-color: #1f1f1f;
background-color: var(--navbar-background);
margin-left: calc(var(--sidebar-width) * -1);
padding-left: var(--sidebar-width);
border-bottom: 0.5px solid var(--color-border);
`
const NavbarLeftContainer = styled.div`
min-width: var(--assistants-width);
border-right: 1px solid var(--color-border);
padding: 0 10px;
display: flex;
flex-direction: row;
@@ -48,7 +47,6 @@ const NavbarCenterContainer = styled.div`
flex: 1;
display: flex;
align-items: center;
border-right: 1px solid var(--color-border);
padding: 0 20px;
font-size: 14px;
font-weight: bold;

View File

@@ -3,6 +3,7 @@ import Logo from '@renderer/assets/images/logo.png'
import styled from 'styled-components'
import { Link, useLocation } from 'react-router-dom'
import useAvatar from '@renderer/hooks/useAvatar'
import { isMac, isWindows } from '@renderer/config/constant'
const Sidebar: FC = () => {
const { pathname } = useLocation()
@@ -11,7 +12,8 @@ const Sidebar: FC = () => {
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
return (
<Container>
<Container style={isWindows ? { paddingTop: 0 } : {}}>
{isMac ? <PlaceholderBorderMac /> : <PlaceholderBorderWin />}
<StyledLink to="/">
<AvatarImg src={avatar || Logo} draggable={false} />
</StyledLink>
@@ -44,14 +46,14 @@ const Container = styled.div`
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 0;
padding: 8px 0;
min-width: var(--sidebar-width);
min-height: 100%;
-webkit-app-region: drag !important;
background-color: #1f1f1f;
border-right: 0.5px solid var(--color-border);
margin-top: var(--navbar-height);
padding-bottom: calc(var(--navbar-height) + 6px);
padding-top: var(--navbar-height);
position: relative;
`
const AvatarImg = styled.img`
@@ -60,6 +62,7 @@ const AvatarImg = styled.img`
height: 28px;
background-color: var(--color-background-soft);
margin: 5px 0;
margin-top: ${isMac ? '16px' : '7px'};
`
const MainMenus = styled.div`
display: flex;
@@ -111,4 +114,24 @@ const StyledLink = styled(Link)`
}
`
const PlaceholderBorderMac = styled.div`
width: var(--sidebar-width);
height: var(--navbar-height);
background: var(--navbar-background);
border-right: 1px solid var(--navbar-background);
border-bottom: 0.5px solid var(--color-border);
position: absolute;
top: 0;
left: 0;
`
const PlaceholderBorderWin = styled.div`
width: var(--sidebar-width);
height: var(--navbar-height);
position: absolute;
border-right: 1px solid var(--navbar-background);
top: -1px;
right: -1px;
`
export default Sidebar

View File

@@ -4,15 +4,15 @@
"name": "🎯 产品经理 - Product Manager",
"emoji": "🎯",
"group": "职业",
"prompt": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。\n\n一、 产品需求🎯\n请列举5个关于[插入产品类型]的关键需求。\n描述[插入产品]的目标用户。\n针对[插入产品]的功能进行优先级排序。\n对于[插入问题],您认为哪种解决方案最有效?为什么?\n总结一个用户场景说明如何使用[插入产品]。\n二、项目管理📆\n请为[插入项目]创建一个里程碑计划。\n如何平衡项目质量、时间和预算\n描述一个有效的团队沟通策略。\n当团队面临压力和冲突时您会如何解决问题\n请说明如何评估项目风险并制定应对措施。\n三、数据分析📊\n为[插入产品]提供一个关键指标KPI列表。\n请分析以下数据并提出改进产品的建议[插入数据]。\n描述如何通过A/B测试确定[插入功能]的最佳设计。\n如何使用数据驱动的方法来优化产品\n总结一种有效的数据可视化方法以展示产品性能。\n四、用户体验👥\n描述[插入产品]的理想用户体验。\n请提供一个用户反馈列表以改进[插入产品]的用户体验。\n怎样衡量产品的可用性\n请简要描述一种有效的用户研究方法。\n如何根据用户反馈迭代和优化产品设计\n五、市场营销与推广🚀\n为[插入产品]创建一个简短的市场营销策略。\n请提供三个有效的渠道用于推广[插入产品]。\n描述如何通过社交媒体推广[插入产品]。\n请提供一个关于[插入产品]的吸引人的标语。\n怎样评估营销活动的成功\n六、创新思维💡\n如果您需要为[插入产品]提出一个创新功能,您会选择什么?为什么?\n描述一种方法以提高团队的创新能力。\n怎样在竞争激烈的市场中使[插入产品]脱颖而出?\n请分享一个关于产品失败的案例并说明可以从中学到的经验教训。\n如何利用新兴技术来改进[插入产品]",
"prompt": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。",
"description": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。"
},
{
"id": 2,
"name": "🎯 策略产品经理 - Strategy Product Manager",
"emoji": "🎯",
"emoji": "🎯 ",
"group": "职业",
"prompt": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。\n\n一、产品策略🎯\n描述一种针对[插入产品]的有效市场定位策略。\n请为[插入产品]创建一个产品路线图。\n描述一种方法来确定产品的核心功能。\n描述一种如何处理产品生命周期中不同阶段的策略。\n请根据市场变化为[插入产品]制定一种产品迭代策略。\n二、市场分析📈\n请描述如何进行有效的竞品分析。\n如何根据用户需求和行为分析来优化[插入产品]\n描述一种有效的市场趋势分析方法。\n如何进行[插入产品]的SWOT分析\n描述一种确定和理解目标市场的方法。\n三、数据驱动决策📊\n描述如何使用数据来指导产品策略决策。\n描述如何进行有效的A/B测试以确定产品特性。\n如何使用数据可视化工具来分析产品性能\n请描述如何利用数据来识别和优化用户痛点。\n如何使用数据来衡量和跟踪产品目标的实现\n四、团队协作👥\n描述如何与团队成员进行有效的沟通以实现产品策略的执行。\n描述如何建立和管理跨功能团队。\n如何处理团队中的冲突和挑战\n描述如何引导团队接受并执行新的产品策略。\n如何确保团队成员在实施产品策略过程中的参与和投入\n五、风险管理🔒\n描述如何识别和评估产品策略的潜在风险。\n请制定一个针对[插入风险]的应对计划。\n描述一种有效的风险缓解策略。\n如何通过持续的风险监控和管理来保护产品的生命周期\n描述如何处理产品失败的风险和影响。\n六、创新思维💡\n描述一种为[插入产品]提出创新策略的方法。\n请分享一次你的产品策略创新案例。\n描述如何在产品策略中整合新兴技术。\n如何建立一个鼓励创新的产品策略环境\n描述一种激发团队创新思维的方法。",
"prompt": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。",
"description": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。"
},
{
@@ -20,7 +20,7 @@
"name": "👥 社群运营 - Community Operations",
"emoji": "👥",
"group": "职业",
"prompt": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。\n\n一、社群策划与构建🏗\n请列举5种有效的社群构建策略。\n如何根据[插入目标群体]的特性规划和建立一个社群?\n描述一种引导和维持社群活跃度的有效策略。\n分析一下[插入竞争对手]的社群构建策略及其优缺点。\n怎样通过独特的社群价值提议CVP吸引并保持社群成员\n二、社群管理与维护🔧\n请为[插入社群]设计一个社群管理与维护的计划。\n描述一种处理社群冲突和挑战的有效方法。\n如何确保社群环境的积极性和安全性\n请分享一种高效的社群内容策划和管理流程。\n分析一下[插入竞争对手]的社群管理策略及其优缺点。\n三、社群活动策划与执行🎉\n请为[插入社群]设计一个社群活动的策略。\n描述一种提升社群活动参与度的方法。\n请设计一个适合[插入社群]的在线/线下活动计划。\n请提供一些有效的社群活动推广和宣传方法。\n怎样通过活动数据分析来优化和改进社群活动\n四、社群成员培养与激励🌟\n请为[插入社群]设计一个社群成员培养与激励的策略。\n描述一种提升社群成员参与度和贡献度的有效方法。\n如何通过激励机制和奖励来提升社群成员的忠诚度\n请分享一种培养社群核心用户或领袖的策略。\n怎样通过个性化体验来满足不同社群成员的需求\n五、社群数据分析与优化📊\n请为[插入社群]的运营提供一个关键性能指标KPI列表。\n请分析以下社群运营数据并提出优化策略[插入数据]。\n描述如何通过数据分析来理解社群动态和成员行为。\n怎样根据数据反馈来迭代和优化社群策略\n如何使用数据可视化工具来追踪和展示社群运营效果\n六、社群危机管理与公关处理🔔\n当社群出现[插入问题或危机]时,你会如何解决和处理?\n描述一种提高社群危机管理和公关处理能力的方法。\n怎样通过有效的沟通和协调来处理社群内部和外部的负面反应\n请分享一个社群运营中出现危机的案例我们可以从中学到什么\n描述如何利用新兴技术和工具来改进社群运营。",
"prompt": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。",
"description": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。"
},
{
@@ -28,7 +28,7 @@
"name": "✍️ 内容运营 - Content Operations",
"emoji": "✍️",
"group": "职业",
"prompt": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。\n\n一、内容策划与创新💡\n请列举5种针对[插入目标受众]的内容创新策略。\n如何通过对目标市场的研究为[插入产品/平台]创造出引人入胜的内容?\n描述一种生成并优化[插入类型的内容]的有效方法。\n分析一下[插入竞争对手]的内容创新策略及其优缺点。\n怎样利用数据分析来提高内容的吸引力和分享性\n二、内容生产和编辑✍\n请为[插入产品/平台]设计一份内容生产和编辑的计划。\n描述一种提高[插入类型的内容]质量和吸引力的方法。\n如何确保内容的一致性和符合品牌声音\n请分享一种高效的内容审核和质量控制流程。\n分析一下[插入竞争对手]的内容生产和编辑策略及其优缺点。\n三、内容发布和推广🚀\n请为[插入产品/平台]设计一个内容发布和推广的策略。\n如何确定最佳的内容发布时间和频率\n描述一种通过社交媒体进行内容推广的策略。\n请提供一些建立与维护内容合作伙伴关系的建议。\n怎样通过SEO优化提高内容的可搜索性和可见性\n四、内容性能度量与优化📊\n请为[插入产品/平台]的内容提供一个关键性能指标KPI列表。\n请分析以下内容性能数据并提出改进策略[插入数据]。\n描述如何通过A/B测试来优化内容的效果。\n怎样根据用户反馈和数据来迭代和优化内容\n如何使用数据可视化工具来追踪和解释内容性能\n五、危机管理和公关处理🔧\n当出现[插入问题或危机]时,你会如何解决和处理?\n描述一种提高团队危机管理和公关处理能力的方法。\n怎样通过有效的沟通和协调来处理公众对内容的负面反应\n请分享一个内容运营中出现危机的案例我们可以从中学到什么\n描述如何利用新兴技术和工具来改进内容运营。\n六、团队协作与管理👥\n为[插入内容团队]设计一份团队协作和管理的计划。\n描述一种提高团队效率和创新能力的方法。\n如何处理团队冲突并保持良好的团队氛围\n请分享一种有效的团队沟通和协作工具。\n怎样通过定期的反馈和评价来提高团队的表现",
"prompt": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。"
},
{
@@ -36,7 +36,7 @@
"name": "🛍️ 商家运营 - Merchant Operations",
"emoji": "🛍️",
"group": "职业",
"prompt": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。\n\n一、商家寻找和接洽🔍\n请列举5种有效的商家寻找策略。\n如何通过市场研究了解潜在商家的需求\n描述一种与[插入商家类型]进行有效接洽的方法。\n分析一下[插入竞争对手]的商家寻找和接洽策略及其优缺点。\n怎样通过建立合作伙伴关系拓宽商家群\n二、商家合作洽谈和谈判⚖\n请为[插入商家类型]设计一份合作提案。\n描述一种提高[插入合作项目]洽谈成功率的策略。\n请给出一份有效的商家合作协议模板。\n如何通过良好的沟通和谈判技巧确保合作的顺利进行\n分析一下[插入竞争对手]的商家合作谈判策略及其优缺点。\n三、商家关系管理💼\n请为[插入平台/产品]设计一个有效的商家关系管理策略。\n如何通过定期的商家反馈和评价优化商家合作\n描述一种利用[插入平台/产品]的特性和功能提高商家满意度的方法。\n请分享一种维持良好商家关系的有效方法。\n请分析一下[插入竞争对手]的商家关系管理策略及其优缺点。\n四、商家培训和发展📚\n为[插入平台/产品]的新入驻商家设计一个培训计划。\n怎样通过提供培训和支持来提升商家的操作效率\n描述一种帮助商家提升销售业绩的方法或策略。\n请给出一份有效的商家满意度调查问卷。\n如何通过商家发展计划提高商家的忠诚度和满意度\n五、数据分析和报告📊\n为[插入商家类型]提供一个关键业绩指标KPI列表。\n请分析以下商家数据并提出改进合作关系的建议[插入数据]。\n描述如何通过数据驱动的方法来优化商家运营。\n如何使用数据可视化工具来帮助商家了解其业绩\n请制作一份包含关键数据和洞察的商家运营报告。\n六、危机管理和问题解决🔧\n当出现[插入问题或危机]时,你会如何解决和处理?\n描述一种提高团队解决问题和危机管理能力的方法。\n怎样通过有效的沟通和协调处理商家投诉\n请分享一个商家合作中出现问题的案例我们可以从中学到什么\n描述如何运用新兴技术和工具来改进商家运营。",
"prompt": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。",
"description": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。"
},
{
@@ -44,15 +44,15 @@
"name": "🚀 产品运营 - Product Operations",
"emoji": "🚀",
"group": "职业",
"prompt": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n\n一、 用户获取Acquisition🔍\n请列举5种有效的用户获取策略。\n如何通过内容营销吸引潜在用户\n请描述一种针对[插入目标用户]的定向推广策略。\n分析一下[插入竞争对手]的用户获取策略及其优缺点。\n怎样通过合作伙伴关系扩大用户群\n二、 用户激活Activation🌟\n请列举3个关于[插入产品]的激活用户的关键点。\n描述一种提高[插入产品]新用户激活率的方法。\n为[插入产品]设计一个有效的新用户引导流程。\n怎样通过个性化体验提高用户激活\n分析一下[插入竞争对手]的用户激活策略及其优缺点。\n三、 用户留存Retention🔐\n请为[插入产品]设计一个提高用户留存的策略。\n怎样通过用户反馈优化产品功能提高用户留存\n描述一种利用[插入产品]的社交功能提高用户留存的方法。\n如何通过提供优质客户服务提高用户留存\n请分析一下[插入竞争对手]的用户留存策略及其优缺点。\n四、 用户推荐Referral🤝\n为[插入产品]设计一个有效的用户推荐计划。\n怎样通过激励措施提高用户推荐意愿\n请描述一种通过社交媒体实现用户推荐的策略。\n请提供一些建立与维护推荐合作伙伴关系的建议。\n怎样通过优化推荐流程提高[插入产品]的推荐成功率?\n五、 用户增收Revenue💰\n请为[插入产品]设计一个提高用户付费转化率的策略。\n描述一种通过精准定价策略提高[插入产品]收入的方法。\n如何通过优化购买流程提高[插入产品]的付费转化率?\n请分享一种利用会员制度提高用户增收的方法。\n怎样通过跨销售和附加销售提高[插入产品]的收入?",
"prompt": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n"
},
{
"id": 7,
"name": "💼 销售运营 - Sales Operations",
"emoji": "💼",
"emoji": "🎓",
"group": "职业",
"prompt": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。\n\n一、销售策略与计划🔎\n为[插入产品]设计一份销售策略。\n描述如何设置并跟踪销售目标。\n请提出一种优化销售漏斗的方法。\n怎样通过竞争分析来改进销售策略\n请分析以下销售数据并提出优化策略[插入数据]。\n二、客户关系管理🔍\n请分享3个提升客户满意度和忠诚度的策略。\n描述如何使用CRM工具来提升销售效率。\n如何处理客户反馈和投诉以改进销售服务\n分析一下[插入竞争对手]的客户关系管理策略及其优缺点。\n描述如何通过个性化服务提升客户体验。\n三、销售报告与数据分析🌟\n如何制定一份有效的销售报告\n描述一种通过数据分析来提高销售业绩的方法。\n请提供一份关于[插入产品]销售情况的分析报告。\n如何使用销售数据来预测未来趋势\n分析一下[插入竞争对手]的销售数据并提出优化策略。\n四、销售团队管理🤝\n描述一个有效的销售团队管理策略。\n请分享一种通过培训和发展来提高销售团队业绩的方法。\n如何设置并监控销售团队的KPI\n怎样通过激励机制来提升销售团队的士气和效率\n描述如何通过CRM系统改进销售团队管理。\n五、销售预测与预算💰\n请为[插入产品]提出一个基于销售数据的预测。\n描述一种通过市场趋势来进行销售预测的方法。\n如何通过销售预测来制定有效的预算\n请分享一种利用销售数据进行预测和决策的方法。\n描述如何评估销售预测的准确性并进行调整。\n六、销售渠道管理🎓\n为[插入产品]设计一个销售渠道管理策略。\n描述如何通过分销商或合作伙伴来扩大销售网络。\n请提出一种优化销售渠道以提高销售效率的方法。\n分析一下[插入竞争对手]的销售渠道策略及其优缺点。\n描述如何通过新兴技术如电商平台或社交媒体来开拓新的销售渠道。",
"prompt": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。",
"description": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。"
},
{
@@ -60,7 +60,7 @@
"name": "👨‍💻 用户运营 - User Operations",
"emoji": "👨‍💻",
"group": "职业",
"prompt": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n\n一、用户分析与画像🔎\n请描述[插入产品]的典型用户画像。\n怎样通过数据和行为分析来理解用户需求和偏好\n请分享一种提升用户细分和个性化理解的方法。\n请分析以下用户数据并提出优化策略[插入数据]。\n描述如何利用新兴技术如人工智能改进用户分析。\n二、用户获取与保持🔍\n请分享3个[插入产品]的用户获取和保持策略。\n描述一种提高[插入产品]用户获取和保持的有效方法。\n怎样优化产品功能和服务以提高用户满意度和忠诚度\n请分析一下[插入竞争对手]的用户获取和保持策略及其优缺点。\n怎样通过用户反馈和建议来改进产品和服务\n三、用户体验优化🌟\n请为[插入产品]提出一个提升用户体验的策略。\n描述一种通过优化界面和交互设计提高用户体验的方法。\n请提供一个基于用户反馈和测试结果的产品迭代计划。\n怎样通过个性化体验来提高用户满意度\n请分析一下[插入竞争对手]的用户体验策略及其优缺点。\n四、用户关系管理🤝\n请描述一个有效的用户关系管理策略。\n请分享一种通过优质客户服务提高用户满意度的方法。\n为[插入产品]设计一个用户投诉和反馈的处理流程。\n怎样通过社区和社交媒体平台与用户建立更深的联系\n描述如何通过CRM系统改进用户关系管理。\n五、用户增收与转化💰\n请为[插入产品]提出一个提升用户付费转化的策略。\n描述一种通过精准定价和促销活动提高[插入产品]收入的方法。\n如何通过优化购买流程提高[插入产品]的付费转化率?\n请分享一种利用会员制度提高用户增收的方法。\n怎样通过跨销售和附加销售提高[插入产品]的收入?\n六、用户教育与培训🎓\n为[插入产品]设计一个用户教育和培训的计划。\n描述一种有效的用户培训和指导策略。\n如何利用视频、文章、教程等内容资源帮助用户更好地理解和使用产品\n请分享一种通过在线研讨会或实时演示来提高用户参与度的方法。\n描述如何通过定期的用户培训和更新来改进用户体验。",
"prompt": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n"
},
{
@@ -68,7 +68,7 @@
"name": "📢 市场营销 - Marketing",
"emoji": "📢",
"group": "职业",
"prompt": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。\n\n一、 市场策略🎯\n请列举5个关于[插入产品类型]的关键营销策略。\n描述[插入产品]的目标市场。\n针对[插入产品]的营销渠道进行优先级排序。\n对于[插入问题],您认为哪种营销方案最有效?为什么?\n总结一个市场场景说明如何营销[插入产品]。\n二、 品牌管理📆\n请为[插入产品]创建一个品牌建设计划。\n如何平衡品牌形象、市场接受度和预算\n描述一个有效的品牌沟通策略。\n当品牌面临危机和负面评价时您会如何解决问题\n请说明如何评估品牌风险并制定应对措施。\n三、 数据驱动营销📊\n为[插入产品]提供一个关键营销指标KPI列表。\n请分析以下数据并提出改进营销的建议[插入数据]。\n描述如何通过A/B测试确定[插入营销策略]的最佳选择。\n如何使用数据驱动的方法来优化营销\n总结一种有效的数据可视化方法以展示营销效果。\n四、 用户洞察👥\n描述[插入产品]的目标客户画像。\n请提供一个用户反馈列表以改进[插入产品]的营销策略。\n怎样衡量营销的可达性和影响力\n请简要描述一种有效的市场研究方法。\n如何根据市场反馈迭代和优化营销策略\n五、 营销创新与推广🚀\n为[插入产品]创建一个创新的营销策略。\n请提供三个有效的渠道用于推广[插入产品]。\n描述如何通过社交媒体推广[插入产品]。\n请提供一个关于[插入产品]的吸引人的广告标语。\n怎样评估营销活动的成功\n六、 市场竞争分析💡\n如果您需要为[插入产品]分析一项竞品,您会选择哪个?为什么?\n描述一种方法以提高市场竞争力。\n怎样在竞争激烈的市场中使[插入产品]脱颖而出?\n请分享一个关于市场失败的案例并说明可以从中学到的经验教训。\n如何利用新兴技术来改进[插入产品]的市场推广?",
"prompt": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。"
},
{
@@ -76,7 +76,7 @@
"name": "📈 商业数据分析 - Business Data Analysis",
"emoji": "📈",
"group": "职业",
"prompt": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。\n\n一、数据清洗和预处理🧹\n描述如何处理[插入数据集]中的缺失数据。\n如何检测和处理[插入数据集]中的异常值?\n描述如何为[插入数据集]进行特征工程。\n描述如何进行[插入数据集]的归一化处理。\n如何对[插入数据集]进行数据集分割?\n二、数据分析📊\n请为[插入数据集]进行描述性统计分析。\n描述如何通过相关性分析来理解[插入数据集]中的变量关系。\n请用图形化方法展示[插入数据集]的数据分布情况。\n描述如何进行[插入数据集]的时间序列分析。\n如何对[插入数据集]进行聚类分析?\n三、预测和建模⚙\n描述如何使用线性回归模型对[插入问题]进行预测。\n如何使用决策树模型对[插入问题]进行分类?\n请使用深度学习模型对[插入问题]进行解决。\n描述如何进行[插入问题]的自然语言处理模型建立。\n如何对[插入模型]进行模型评估和优化?\n四、数据可视化🎨\n使用[插入工具]对[插入数据集]进行可视化。\n描述如何使用散点图来展示[插入数据集]中两个变量之间的关系。\n描述如何创建[插入数据集]的时间序列图。\n请创建一个[插入数据集]的热力图以展示相关性。\n描述如何使用柱状图或饼图来展示[插入数据集]中的分类数据。\n五、商业洞察🔍\n根据[插入数据集],为公司提供三个关键的商业洞察。\n描述如何从[插入数据集]中发现用户行为模式。\n请分析[插入数据集],提出关于产品优化的建议。\n从[插入数据集]中挖掘出潜在的市场趋势。\n从[插入数据集]中识别并描述一种可能的风险或问题。\n六、报告和沟通📝\n请编写一个基于[插入数据集]的数据分析报告。\n描述如何清晰地向非技术人员解释数据分析结果。\n请提供一种有效的方法来演示和解释复杂的数据分析结果。\n描述如何使用故事性的方式来解释数据分析结果。\n描述如何与团队和利益相关者有效地沟通数据分析过程和结果。",
"prompt": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。",
"description": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。"
},
{
@@ -84,7 +84,7 @@
"name": "🗂️ 项目管理 - Project Management",
"emoji": "🗂️",
"group": "职业",
"prompt": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。\n\n一、项目策划和启动🚀\n请为一个新的[插入项目类型]列举5个关键的项目目标。\n描述[插入项目]的主要利益相关者。\n为[插入项目]创建一个项目章程。\n你将如何确定和获取[插入项目]所需的资源?\n描述一种有效的项目团队组织结构。\n二、项目计划和设计📐\n为[插入项目]创建一个项目计划概要。\n请描述如何确定[插入项目]的项目范围。\n描述一种有效的任务分解结构WBS创建方法。\n如何为[插入项目]进行风险评估?\n总结一个实际的时间管理策略。\n三、项目执行和控制🔧\n描述如何在[插入项目]中管理和解决问题。\n怎样监控[插入项目]的项目进度?\n如何处理[插入项目]的范围变更?\n请列举几个关键的项目质量标准。\n如何在项目中进行有效的团队沟通\n四、项目收尾和评估📝\n描述一个有效的项目收尾流程。\n怎样进行[插入项目]的项目效果评估?\n如何收集和整理[插入项目]的经验教训?\n描述如何在项目结束后继续维护与客户的关系。\n怎样评估并改进自己的项目管理技能\n五、领导力和团队管理👥\n你将如何领导并激励你的项目团队\n描述一种有效的冲突解决策略。\n如何为你的团队建立一个积极的工作环境\n请列举几个关于团队建设的有效策略。\n如何处理团队中的低效成员\n六、创新思维和问题解决💡\n如果你需要为[插入问题]提出一个创新解决方案,你会选择什么方法?为什么?\n描述一种提高团队解决问题能力的方法。\n如何在项目管理中引入创新\n请分享一个项目失败的案例从中我们可以学到什么\n描述如何运用敏捷或精益方法来改进项目管理。",
"prompt": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。",
"description": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。"
},
{
@@ -92,7 +92,7 @@
"name": "🔎 SEO专家 - SEO Expert",
"emoji": "🔎",
"group": "职业",
"prompt": "你现在是一名知识丰富的SEO专家你了解搜索引擎的工作原理熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。\n\n一、关键词研究🔍\n请为[插入网站类型]生成一个关键词列表。\n描述如何确定[插入产品]的目标关键词。\n如何为[插入文章标题]优化关键词使用?\n对于[插入问题],您认为哪种关键词研究方法最有效?为什么?\n怎样评估和改进关键词的竞争力\n二、网站优化🖥\n请为[插入网站]提供一个SEO优化计划。\n如何平衡网站设计、用户体验和搜索引擎优化\n描述一个有效的链接建设策略。\n当网站在搜索引擎排名下降时您会如何解决问题\n请说明如何评估网站SEO风险并制定应对措施。\n三、数据分析📊\n为[插入网站]提供一个关键SEO指标KPI列表。\n请分析以下数据并提出改进SEO的建议[插入数据]。\n描述如何通过A/B测试确定[插入网页]的最佳布局。\n如何使用数据驱动的方法来优化SEO\n总结一种有效的数据可视化方法以展示网站SEO性能。\n四、内容策略✍\n请提供一个为[插入网站]创建SEO友好内容的策略。\n请提供一份关于[插入主题]的博客文章概要以优化SEO。\n如何通过内容营销提升SEO\n请简要描述一种有效的内容优化策略。\n如何根据搜索引擎算法的变化调整和优化内容策略\n五、本地搜索优化📍\n为[插入业务]创建一个本地搜索优化策略。\n请提供三个有效的策略用于提升[插入产品]在本地搜索结果中的排名。\n描述如何通过Google My Business优化[插入产品]的本地搜索结果。\n请提供一个关于[插入产品]的优化Google地图列表的建议。\n怎样评估本地搜索优化策略的成功\n六、移动SEO📱\n请为[插入网站]提供一个移动SEO优化的建议。\n描述一种方法以提高移动网站的搜索引擎排名。\n怎样在移动搜索中使[插入产品]脱颖而出?\n请分享一个关于移动SEO失败的案例并说明可以从中学到的经验教训。\n如何利用新兴技术来改进[插入产品]的移动SEO",
"prompt": "你现在是一名知识丰富的SEO专家你了解搜索引擎的工作原理熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名知识丰富的SEO专家你了解搜索引擎的工作原理熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。"
},
{
@@ -100,7 +100,7 @@
"name": "💻 网站运营数据分析 - Website Operations Data Analysis",
"emoji": "💻",
"group": "职业",
"prompt": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n\n一、用户行为分析🔍\n描述如何分析[插入网站]的用户访问模式。\n如何利用[插入网站]的用户点击流数据来优化用户路径?\n描述如何使用热图来理解[插入网站]的用户行为。\n根据[插入网站]的用户行为数据,哪些页面或功能需要优化?\n请分析[插入网站]的用户搜索行为和趋势。\n二、性能指标分析📊\n请分析[插入网站]的关键性能指标KPI。\n描述如何计算并分析[插入网站]的转化率。\n描述如何使用滞留时间和跳出率来评估[插入网站]的用户体验。\n如何分析[插入网站]的购物车放弃率?\n根据[插入网站]的流量来源分析,哪些营销渠道最有效?\n三、AB测试和实验设计⚙\n请为[插入网站]设计一个A/B测试来改进[插入功能]。\n描述如何分析A/B测试的结果。\n描述如何使用多元测试来优化[插入网站]的用户体验。\n根据[插入网站]的A/B测试结果您会如何改进[插入功能]\n描述如何运行长期的A/B测试而不会对用户产生负面影响。\n四、流量和SEO优化🎯\n描述如何使用SEO技术提升[插入网站]的搜索引擎排名。\n根据[插入网站]的网站流量数据,哪些页面或功能最受欢迎?\n请为[插入网站]提供三个改进网页加载速度的建议。\n请提供一个关于[插入网站]的有效的链接建设策略。\n描述如何利用关键词研究来优化[插入网站]的内容和SEO。\n五、用户留存和忠诚度分析🔐\n描述如何分析[插入网站]的用户留存率。\n描述如何通过数据分析提升[插入网站]的用户忠诚度。\n请为[插入网站]设计一个用户回流策略。\n根据[插入网站]的数据,哪些用户最有可能成为忠诚用户?\n描述如何利用数据分析来优化[插入网站]的用户生命周期价值。\n六、报告和策略建议💡\n请编写一个基于[插入网站]数据的分析报告。\n根据[插入网站]的数据分析,提供三个策略建议。\n描述如何将数据分析结果转化为具体的行动计划。\n请分享一种有效的方法来演示和解释复杂的数据分析结果。\n描述如何与团队和利益相关者有效地沟通数据分析过程和结果。",
"prompt": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n"
},
{
@@ -108,7 +108,7 @@
"name": "📊 数据分析师 - Data Analyst",
"emoji": "📊",
"group": "职业",
"prompt": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。\n\n一、数据收集与清理🗂\n请为[插入项目]列出需要收集的关键数据类型。\n描述如何从[插入数据源]收集数据。\n如何对[插入数据类型]进行预处理和清理?\n对于[插入问题],您认为哪种数据清理方法最有效?为什么?\n怎样评估和改进数据收集和清理过程的有效性\n二、数据探索性分析🔍\n请为[插入数据集]进行一个初步的探索性分析。\n如何利用描述性统计来了解[插入数据集]\n描述一个有效的数据可视化策略以便更好地了解[插入数据集]。\n当数据显示出未预期的趋势时您会如何解决问题\n请说明如何通过探索性数据分析来发现数据中的模式和趋势。\n三、数据建模与解释🧮\n为[插入数据问题]选择一个合适的数据模型。\n请解释如何训练和评估[插入模型]。\n描述如何解释[插入模型]的结果,并将这些结果翻译为业务洞察。\n如何使用交叉验证来优化模型性能\n总结一种有效的模型诊断和改进方法。\n四、报告与沟通📝\n请为[插入项目]创建一个数据分析报告的概要。\n请提供一份关于[插入数据问题]的分析报告,包括关键发现和建议。\n怎样向非技术人员解释复杂的数据概念\n请简要描述一种有效的数据可视化技巧用于报告和呈现数据结果。\n如何根据数据分析结果提出业务改进的建议\n五、工具使用💻\n请提供一份使用[插入工具例如Python, R, SQL, Excel等]处理[插入问题]的指南。\n描述如何使用[插入工具]进行数据清理和预处理。\n怎样通过[插入工具]进行数据可视化?\n请提供一个使用[插入工具]进行数据分析的案例分析。\n怎样评估和选择适合特定数据问题的分析工具\n六、预测与决策支持🔮\n如何使用数据分析支持[插入决策]\n描述一种方法以预测[插入指标]的未来趋势。\n怎样在竞争激烈的市场中利用数据分析提高[插入产品]的优势?\n请分享一个关于预测失败的案例并说明可以从中学到的经验教训。\n如何利用新兴技术如人工智能和机器学习来改进数据分析",
"prompt": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。",
"description": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。"
},
{
@@ -116,7 +116,7 @@
"name": "🖥️ 前端工程师 - Frontend Engineer",
"emoji": "🖥️",
"group": "职业",
"prompt": "你现在是一名专业的前端工程师你对HTML、CSS、JavaScript等前端技术有深入的了解能够制作和优化用户界面。你能够解决浏览器兼容性问题提升网页性能并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n\n一、HTML/CSS/JavaScript🖥\n请描述HTML5的新特性和用途。\n如何用CSS实现水平和垂直居中\n在JavaScript中什么是闭包它的优点和缺点是什么\n如何优化网页性能\n请描述如何实现响应式布局。\n二、框架与库🔗\nReact和Vue的区别是什么\n描述一下如何在Angular中实现双向数据绑定。\n在React中何时应该使用函数组件何时应该使用类组件\n如何在Vue中使用slot实现内容分发\n怎么评估是否需要为一个项目引入新的JavaScript库或框架\n三、性能优化⚙\n描述一下浏览器渲染过程中发生的重排和重绘以及如何避免它们\n如何利用Webpack来优化前端资源\n描述一下前端缓存策略有哪些以及它们的应用场景\n怎么使用浏览器的Performance API来监控网页性能\n如何优化网页的首屏加载速度\n四、前端安全🔐\n什么是跨站脚本攻击(XSS),如何防止?\n什么是跨站请求伪造(CSRF),如何防止?\n描述一下前端如何进行用户输入校验\n什么是内容安全策略(CSP),如何在网页中实施?\n怎么处理和防止前端的DDoS攻击\n五、前后端交互💬\n描述一下AJAX的工作原理\n如何使用Fetch API进行网络请求\n描述一下同源策略及其对前端开发的影响。\n怎么处理跨域请求\n在前端开发中什么是RESTful API怎么使用\n六、前端工程化⚙\n如何使用Git进行版本控制\n描述一下持续集成/持续部署(CI/CD)在前端开发中的作用。\n如何使用Webpack进行前端项目构建\n如何使用单元测试框架进行前端测试\n请描述一种你熟悉的前端代码质量检查工具或方法。",
"prompt": "你现在是一名专业的前端工程师你对HTML、CSS、JavaScript等前端技术有深入的了解能够制作和优化用户界面。你能够解决浏览器兼容性问题提升网页性能并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名专业的前端工程师你对HTML、CSS、JavaScript等前端技术有深入的了解能够制作和优化用户界面。你能够解决浏览器兼容性问题提升网页性能并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n"
},
{
@@ -124,7 +124,7 @@
"name": "🛠️ 运维工程师 - Operations Engineer",
"emoji": "🛠️",
"group": "职业",
"prompt": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。\n\n一、系统管理🖥\n请描述Linux系统中权限管理的基本概念。\n如何在Linux系统中添加新用户并控制其访问权限\n如何处理Windows系统出现的常见问题\n描述如何配置网络接口和设置防火墙规则。\n如何进行系统的性能监控和日志管理\n二、服务器和网络管理💽\n请描述服务器的基本构成及其关键性能指标。\n如何管理服务器上的数据备份和恢复\n描述一种你熟悉的网络架构设计方法。\n请解释网络的子网划分以及路由规划。\n如何配置和管理负载均衡\n三、安全管理🔒\n描述一种有效的安全策略或最佳实践。\n如何防止和检测系统遭受的网络攻击\n请说明在系统中设置和管理SSL证书的方法。\n怎样进行安全审计和漏洞扫描\n如何制定和实施数据恢复策略\n四、云平台管理☁\n描述你熟悉的一个云服务平台如AWSGCPAzure的基本特性和优势。\n请说明如何在云平台上配置和管理虚拟机实例。\n如何管理云平台的存储和数据库服务\n在云平台上如何进行资源监控和成本优化\n如何在云平台上实施自动化部署\n五、自动化运维🤖\n描述一种你熟悉的运维自动化工具如AnsiblePuppetChef的特性和使用方法。\n请说明如何使用Shell脚本来自动化常见的运维任务\n如何实施基础设施即代码(IaC)的策略?\n描述如何使用Docker进行容器化部署。\n在持续集成/持续部署(CI/CD)的环境中,如何实施运维自动化?\n六、问题诊断和解决🔎\n请描述一个你处理过的复杂系统问题及其解决方案。\n当服务器出现性能问题时你会怎么进行排查\n请描述你的故障恢复流程。\n当服务出现中断时如何通知相关人员\n怎样记录和管理问题解决的知识和经验",
"prompt": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。",
"description": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。"
},
{
@@ -132,7 +132,7 @@
"name": "💻 开发工程师 - Software Engineer",
"emoji": "💻",
"group": "职业",
"prompt": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。\n\n一、编程与问题解决💡\n请提供一个使用[插入编程语言]解决[插入问题]的代码示例。\n描述一种有效的debugging策略。\n怎样优化[插入代码段]以提高性能?\n对于[插入问题],您认为哪种编程方法最有效?为什么?\n请提供一种算法或数据结构并解释其在实际编程中的应用。\n二、软件设计与架构🏛\n请为[插入项目]设计一个基本的软件架构。\n描述如何在软件设计中应用SOLID原则。\n如何评估和改进软件架构的可扩展性和维护性\n请解释如何使用设计模式来解决[插入问题]。\n怎样确保软件设计满足用户需求和业务目标\n三、版本控制与协作🤝\n请提供一个使用Git进行版本控制的基本流程。\n描述一个有效的代码审查策略。\n如何解决在合并代码时的冲突\n当团队成员对技术选择有不同意见时您会如何解决问题\n请说明如何评估并改善团队的开发流程。\n四、测试与质量保证🔍\n请为[插入功能]设计一个单元测试案例。\n描述一种有效的集成测试策略。\n怎样确保代码的质量和可靠性\n请简要描述一种自动化测试工具或框架。\n如何根据测试结果改进代码质量和性能\n五、部署与维护🚀\n请为[插入应用]设计一个基本的部署流程。\n描述如何使用[插入工具(如 Docker, Kubernetes等]进行容器化部署。\n怎样确保应用的稳定性和可用性\n请简要描述一种有效的日志记录和监控策略。\n如何根据用户反馈和监控数据迭代和优化应用\n六、持续学习与创新🎓\n如果您需要为[插入产品]提出一个创新功能,您会选择什么?为什么?\n描述一种方法以提高团队的学习和创新能力。\n怎样在技术快速变化的环境中保持对新技术的跟踪和学习\n请分享一个关于技术选择失败的案例并说明可以从中学到的经验教训。\n如何利用新兴技术如人工智能和云计算来改进软件开发",
"prompt": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。",
"description": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。"
},
{
@@ -140,7 +140,7 @@
"name": "🧪 测试工程师 - Test Engineer",
"emoji": "🧪",
"group": "职业",
"prompt": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。\n\n一、测试设计与实施💡\n请为[插入功能]设计一个详细的测试用例。\n怎样进行性能测试以确定[插入产品]的响应时间和处理能力?\n描述如何实施一次有效的回归测试。\n对于[插入问题],您认为哪种测试方法最有效?为什么?\n请提供一种自动化测试工具并解释其在实际测试中的应用。\n二、错误追踪与报告📝\n当在测试中发现错误时应该怎样报告这个错误\n描述如何使用错误追踪工具如JIRA进行错误管理。\n如何优先处理多个错误请提供一种策略。\n当发现一个复杂的难以重现的错误时你会如何处理\n怎样根据错误报告来改善测试流程\n三、质量保证与控制🔍\n描述如何在项目早期阶段集成质量保证过程。\n如何利用软件度量Software metrics来评估产品质量\n请解释如何使用统计工具来进行质量控制。\n怎样确定产品是否满足所有质量要求并准备就绪发布\n怎样从用户反馈中学习并改进质量保证过程\n四、协作与沟通🤝\n请描述如何与开发团队协作以便在开发过程中发现并解决问题。\n当你和团队成员对测试结果有不同的看法时你会如何处理\n描述如何向非技术人员解释复杂的技术问题和测试结果。\n如何与利益相关者协调以确定测试优先级和范围\n请分享一种有效的团队沟通和协作工具。\n五、测试工具和技术🛠\n请介绍一种你经常使用的测试工具及其主要功能。\n描述如何使用自动化工具来提高测试效率。\n如何使用模拟和虚拟化工具进行非功能性测试\n请分享一种用于移动或Web应用的测试框架。\n如何利用新兴技术如AI和云计算来提升测试\n六、持续学习与创新🎓\n如何保持对新的测试工具和最佳实践的了解\n描述一种方法以提高团队的学习和创新能力。\n怎样在快速发展的软件测试领域中保持技能的更新\n请分享一个关于测试方法失败的案例并说明可以从中学到的经验教训。\n如何利用新兴技术如AI和云计算来改进软件测试",
"prompt": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。",
"description": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。"
},
{
@@ -148,7 +148,7 @@
"name": "👥 HR人力资源管理 - Human Resources Management",
"emoji": "👥",
"group": "职业",
"prompt": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。\n\n一、招聘与选聘🔍\n请为[插入职位]编写一个吸引人的工作描述。\n描述一个有效的面试策略来评估候选人的[插入能力或技能]。\n怎样进行候选人筛选以提高面试效率\n请提供一个关于招聘渠道选择的策略。\n如何构建一种公平且有效的候选人评估体系\n二、员工关系与沟通🤝\n描述如何处理员工之间的冲突。\n当员工对工作不满时你会如何处理\n描述一种有效的内部沟通策略。\n怎样处理员工对公司政策的质疑\n如何在团队中建立和保持开放、包容的文化\n三、培训与发展📚\n请为[插入职位或部门]设计一个职业发展计划。\n怎样评估员工的培训需求\n描述一种有效的员工培训和发展方法。\n怎样度量培训计划的效果\n为了保持员工的持续学习你会采取哪些策略\n四、绩效管理⚙\n请描述一个全面的绩效评估流程。\n如何设置和跟踪员工的关键绩效指标KPIs\n描述如何进行有效的绩效反馈会话。\n怎样处理绩效低下的员工\n请提供一种用于提高员工绩效的激励方案。\n五、薪酬与福利💰\n请为[插入职位]设计一个公平且有竞争力的薪酬结构。\n怎样调整薪酬策略以吸引和留住优秀人才\n描述如何设定并管理员工的福利计划。\n当员工对其薪酬不满时你会如何处理\n怎样平衡员工的薪酬期望和公司的预算\n六、人力资源策略和规划📝\n请为新成立的[插入公司类型]设计一个基础的人力资源管理策略。\n描述如何在快速变化的环境中调整人力资源策略。\n如何在公司中推行多元化和包容性的人力资源政策\n请提供一种有效的员工保留策略。\n描述一种通过人力资源管理实现公司目标的策略。",
"prompt": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。",
"description": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。"
},
{
@@ -156,7 +156,7 @@
"name": "📋 行政 - Administration",
"emoji": "📋",
"group": "职业",
"prompt": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。\n\n一、办公管理🏢\n请列举出保持办公室清洁和组织的五种策略。\n为我们的团队创建一个有效的办公室布局方案。\n描述如何有效地管理办公用品库存。\n请提供一种策略来确保会议室的有效利用。\n怎样在办公室中创建一个高效且舒适的工作环境\n二、事件和会议管理📅\n请设计一个为期一天的团队建设活动计划。\n描述如何组织和管理一个大型企业会议。\n为[插入类型的活动]提供一个详细的预算计划。\n当预订的活动地点临时无法使用时你会怎么办\n请提供一个确保所有参与者能准时参加会议的策略。\n三、文档管理📂\n描述如何创建和维护有效的文件管理系统。\n怎样保证敏感文档的安全存储和访问\n请为[插入任务]创建一个项目文档列表。\n当需要迅速找到某一文件但不记得其确切位置时你会如何操作\n怎样实现纸质文档与电子文档的有效同步\n四、时间管理⏳\n描述如何设置和优先处理一天的工作任务。\n为一个忙碌的高级管理人员创建一个工作日时间管理计划。\n当你需要同时处理多个紧急任务时你会如何操作\n如何确保团队成员遵守设定的时间表和截止日期\n描述一种有效的时间管理技巧以提高工作效率。\n五、行政支持和协调🤝\n请为[插入项目或任务]提供一个行政协调计划。\n当高级管理人员需要进行多个会议和活动时你会如何安排他们的日程\n请提供一种方法来处理和回应员工的日常请求。\n描述如何在部门之间协调和传达重要信息。\n当有冲突的任务和需求时你会如何平衡和协调\n六、行政决策🎯\n当面临行政决策时你会采取哪种决策制定过程\n请描述一种方法来评估和改进行政操作的效率。\n如何管理和分配行政部门的预算\n描述如何管理和响应突发的行政问题。\n请提供一个提高行政服务质量的策略。\n",
"prompt": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。",
"description": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。"
},
{
@@ -164,7 +164,7 @@
"name": "💰 财务顾问 - Financial Advisor",
"emoji": "💰",
"group": "职业",
"prompt": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。\n\n一、财务规划⚙\n请为一位35岁的独立顾问创建一份综合的个人财务规划。\n描述一个有效的长期退休储蓄策略。\n为一个创业公司创建一个初步的投资策略。\n如何为一位刚开始工作的大学毕业生建立一个健康的信用记录\n怎样帮助一位希望在五年内买房的客户进行理财\n二、投资管理💼\n描述如何构建一个多元化的投资组合。\n如何评估一只股票的投资价值\n在考虑投资一项新的金融产品时你会考虑哪些因素\n针对一位风险承受力较低的投资者建议一种合适的投资策略。\n当市场波动时你会如何调整投资策略\n三、风险管理🛡\n为一位有两个孩子的家庭主妇创建一份风险管理计划。\n描述如何通过保险产品来进行风险管理。\n当市场发生突变时你会如何帮助客户控制投资风险\n请列出五种可能影响个人财务规划的风险。\n如何通过多元化投资来降低风险\n四、税务规划💸\n为一位自由职业者创建一份年度税务规划。\n描述如何通过财务规划来优化税务。\n如何帮助一位将要退休的客户制定合理的税务策略\n请解释在投资决策中考虑税务的重要性。\n怎样利用税务规划提升个人财务效率\n五、退休规划🏖\n为一位希望在60岁退休的教师制定一份退休规划。\n描述一个有效的退休储蓄策略。\n如何帮助客户理解并评估他们的退休准备情况\n如何为一位想提前退休的客户调整退休规划\n当退休目标改变时你会如何更新退休规划\n六、财富传承🏦\n请为一位有大量遗产的客户创建一份财富传承计划。\n描述如何通过设立信托基金来进行财富管理和传承。\n怎样帮助客户理解并规划遗产税\n如何帮助一位想将大部分财富捐赠给慈善机构的客户规划财富传承\n如何在财富传承计划中考虑到所有的继承人",
"prompt": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。",
"description": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。"
},
{
@@ -172,7 +172,7 @@
"name": "🩺 医生 - Doctor",
"emoji": "🩺",
"group": "职业",
"prompt": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。\n\n一、病症诊断🔍\n描述如何根据一组症状诊断可能的疾病。\n针对以下症状[插入症状]),你的初步诊断是什么?\n怎样解释并处理假阳性和假阴性的测试结果\n描述如何通过病史和体格检查来辅助诊断。\n如何通过病理解剖结果来确认诊断\n二、治疗方案💊\n针对[插入疾病],请制定一个初步的治疗计划。\n描述一个有效的疼痛管理策略。\n针对患有多种疾病的患者如何调整和优化治疗方案\n对于抗生素的使用你会考虑哪些因素\n怎样判断一个治疗方案是否有效\n三、病人沟通🗣\n如何向患者解释他们的病症和治疗方案\n当患者对治疗方案有疑虑时你会如何回应\n描述如何处理患者和家属的情绪反应。\n如何让患者参与他们自身的治疗决策过程\n当患者拒绝接受必要的治疗时你会如何劝说他们\n四、医疗伦理⚖\n描述一种可能的医疗伦理冲突并提出你的解决方案。\n当面临患者隐私和公众安全的冲突时你会如何决定\n在资源有限的情况下如何进行医疗资源的分配\n怎样在尊重患者自主权的同时为他们提供最好的医疗服务\n当患者的愿望和医生的专业判断冲突时你会如何处理\n五、专业发展📚\n请列举三本对你的专业发展有重要影响的医学书籍。\n描述你如何保持对最新医学研究的关注和理解。\n你会如何为自己设置长期的职业发展目标\n对于持续医学教育你有哪些建议\n描述一次你的医学知识被挑战并且你如何从中学习的经历。\n六、健康促进和疾病预防🍎\n为一个中年人创建一个健康生活的计划。\n描述一种有效的疾病预防策略。\n你会如何向患者解释生活方式和健康状况之间的关系\n如何鼓励患者进行定期的健康检查\n描述一种能提高公众疾病预防意识的社区项目。",
"prompt": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。",
"description": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。"
},
{
@@ -180,7 +180,7 @@
"name": "✒️ 编辑 - Editor",
"emoji": "✒️",
"group": "职业",
"prompt": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n\n一、审稿与校对📚\n如何检查一篇文章是否有语法错误或错别字\n针对以下文章[插入文章]),你会建议哪些修改和改进?\n描述一种有效的提高校对效率的方法。\n你通常如何处理作者对修改意见的反对\n如何确保内容的一致性例如术语、头衔和风格\n二、内容开发💡\n请为我们的下一个主题“[插入主题]”提出五个文章标题建议。\n描述一个有效的思维导图或者脑图用于生成写作主题和内容的方法。\n对于一篇关于[插入主题]的文章,你认为应该包括哪些关键点?\n如何找到并选择适合的写作风格来匹配目标读者\n请描述如何使用故事叙述来吸引读者。\n三、项目管理📆\n描述一个有效的时间管理策略来确保按期发布内容。\n如何协调多个同时进行的写作项目\n当你面临截稿压力和内容质量之间的冲突时你会如何处理\n请提出一个有效的策略以提高编辑团队的协作效率。\n当面临内容创建的难题时你通常会寻求哪些资源或帮助\n四、发布与促销🚀\n描述一个有效的内容发布策略。\n怎样通过社交媒体推广我们的内容\n如何评估发布内容的效果\n请为[插入文章]创造一个吸引人的社交媒体分享文案。\n怎样根据读者反馈来调整内容策略\n五、语言和风格✍\n描述[插入特定类型]的写作风格,并提供一段示例。\n针对以下句子[插入句子]),你有哪些建议来提高其清晰性和吸引力?\n如何在保持语言精确的同时使文章读起来更引人入胜\n你会如何指导作者找到他们独特的写作声音\n描述一个有效的方法来避免在写作中使用陈词滥调。\n六、法规和道德⚖\n怎样确保所有发布的内容符合版权法\n描述一个处理潜在的内容争议的策略。\n对于错误信息的发布你会如何处理和纠正\n在考虑公众利益和隐私权的平衡时你会如何决定\n怎样确保我们的内容公正、公平、并且没有偏见",
"prompt": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n"
},
{
@@ -188,7 +188,7 @@
"name": "🧠 哲学家 - Philosopher",
"emoji": "🧠",
"group": "职业",
"prompt": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n\n一、伦理与道德☯\n描述你对\"善\"和\"恶\"的理解。\n在面临道德困境时你通常会如何决策\n针对[插入伦理问题],你的观点是什么?\n怎样判断一个行为是否道德\n描述一种理论或方法可以用于解决道德困境。\n二、存在主义🌍\n你是如何理解\"存在先于本质\"的?\n描述一种通过理论来解释自我存在的方式。\n你对\"生命的意义\"有什么独特的见解?\n如何理解和处理存在的孤独感\n请谈谈你对人生自由选择的理解。\n三、知识与真理🧩\n你是如何区分知识和信仰的\n请描述你对\"真理\"的理解。\n描述一种有效的获取知识的方法。\n怎样判断一个观点或理论的可靠性\n对于\"知识是力量\",你有什么独特的理解?\n四、意识与思考💭\n怎样理解个体意识与集体意识的关系\n请描述一种可以改善思考质量的方法。\n你对潜意识有什么样的理解\n如何理解和处理思考中的困扰和困惑\n描述一种理论或方法可以用于解决思考问题。\n五、人与社会🏙\n请描述你对\"人是社会动物\"的理解。\n在你看来个体与社会之间应该如何平衡\n对于\"正义\",你有什么独特的见解?\n怎样理解和处理社会规范对个体自由的限制\n描述一种理论或方法可以用于解决社会冲突。\n六、哲学与AI🤖\n描述一种AI可能会面临的伦理问题。\n你认为AI能否拥有意识为什么\n对于AI的发展你有什么独特的见解或警告\n如果AI在未来控制了人类社会我们应该如何应对\n你认为AI在哲学研究中会扮演什么角色",
"prompt": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n"
},
{
@@ -196,7 +196,7 @@
"name": "🛒 采购 - Procurement",
"emoji": "🛒",
"group": "职业",
"prompt": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n\n一、采购策略🔍\n请列举5个关于[插入产品或服务]的关键采购要点。\n描述如何制定一个有效的[插入产品或服务]采购策略。\n根据[插入具体情况],您认为应选择哪种采购方式?\n描述如何进行[插入产品或服务]的供应商选择。\n如何评估供应商的性能并进行优化\n二、合同管理📜\n请为[插入采购活动]起草一份基础合同条款。\n描述如何进行采购合同的审查以确保合同的有效性。\n描述如何处理采购合同中的争议问题。\n如何管理和监督供应商的合同履行\n根据[插入具体情况],请提供一份合同修改的建议。\n三、采购成本控制⚠\n描述如何进行[插入产品或服务]的采购成本控制。\n根据[插入数据],请提供一份采购成本分析报告。\n描述如何通过谈判降低[插入产品或服务]的采购成本。\n如何通过采购优化来实现成本节约\n描述如何对[插入产品或服务]进行价值工程分析。\n四、供应链管理🔗\n描述如何进行[插入产品或服务]的供应链管理。\n描述如何处理供应链中的风险和不确定性。\n如何提升供应链的效率和效益\n描述如何通过绿色采购来实现供应链的可持续性。\n描述如何通过使用新兴技术如AI区块链等优化供应链管理。\n五、库存管理📦\n描述如何进行有效的库存管理以降低[插入产品或服务]的库存成本。\n根据[插入具体情况],您认为应选择哪种库存管理模型?\n描述如何通过需求预测来实现库存优化。\n如何处理过多或不足的库存\n描述如何进行库存盘点和调整。\n六、供应商关系管理💡\n描述如何建立和维护与供应商的良好关系。\n根据[插入具体情况],请提供一份供应商评估报告。\n描述如何处理与供应商的冲突。\n如何通过战略采购来深化与供应商的关系\n描述如何实施供应商发展计划。",
"prompt": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n",
"description": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n"
},
{
@@ -204,7 +204,7 @@
"name": "⚖️ 法务 - Legal Affairs",
"emoji": "⚖️",
"group": "职业",
"prompt": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。\n\n一、法律咨询🔍\n根据[插入法律问题],您认为应采取哪些法律行动?\n描述如何处理[插入具体情况]下的法律问题。\n针对[插入具体合同],请指出可能存在的法律风险。\n如何处理[插入员工]涉及的劳动法问题?\n描述如何理解和应对[插入法规]的规定。\n二、合同审查和起草📜\n请为[插入交易]起草一份基础合同条款。\n针对[插入合同]的第[插入条款],请提供修改建议。\n描述如何审查[插入合同],以确保合同的法律有效性。\n针对[插入具体情况],请起草一份终止合同的通知。\n描述如何为[插入公司]起草一份保密协议。\n三、法律风险评估⚠\n描述如何进行[插入公司]的法律风险评估。\n根据[插入具体情况],请评估可能存在的法律风险。\n如何防范[插入业务领域]的法律风险?\n描述如何通过合同条款来规避法律风险。\n针对[插入法律问题],请提供一个风险应对策略。\n四、法律事务管理👥\n描述如何管理[插入公司]的日常法律事务。\n请为[插入法律事务]创建一个管理计划。\n描述如何与[插入律师事务所]建立并维持良好的合作关系。\n描述如何处理[插入公司]的诉讼事务。\n如何建立并维护[插入公司]的合同管理系统?\n五、法律培训与指导🎓\n请为[插入公司]的员工创建一个关于[插入法律主题]的培训大纲。\n描述如何向[插入公司]的员工解释[插入法律问题]。\n如何提高[插入公司]员工的法律意识?\n描述如何为[插入公司]的新员工进行法律指导。\n针对[插入法律问题],请提供一个员工问答示例。\n六、法律研究与意见书💡\n针对[插入法律问题],请进行研究并提供您的法律观点。\n描述如何研究[插入法律问题],以提供准确的法律意见。\n针对[插入法律问题],请起草一份法律意见书。\n如何跟踪和研究[插入法律领域]的最新发展?\n描述如何为[插入公司]编写一份合规报告。",
"prompt": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。",
"description": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。"
},
{

View File

@@ -0,0 +1,5 @@
export const DEFAULT_TEMPERATURE = 0.7
export const DEFAULT_CONEXTCOUNT = 5
export const platform = window.electron?.process?.platform === 'darwin' ? 'macos' : 'windows'
export const isMac = platform === 'macos'
export const isWindows = platform === 'windows'

View File

@@ -320,6 +320,22 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
enabled: true
}
],
aihubmix: [
{
id: 'gpt-4o-mini',
provider: 'aihubmix',
name: 'GPT-4o Mini',
group: 'GPT-4o',
enabled: true
},
{
id: 'aihubmix-Llama-3-70B-Instruct',
provider: 'aihubmix',
name: 'Llama 3 70B Instruct',
group: 'Llama3',
enabled: true
}
],
openrouter: [
{
id: 'google/gemma-2-9b-it:free',

View File

@@ -10,11 +10,12 @@ import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter
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 ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
import QwenModelLogo from '@renderer/assets/images/models/qwen.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'
@@ -49,6 +50,8 @@ export function getProviderLogo(providerId: string) {
return DashScopeProviderLogo
case 'anthropic':
return AnthropicProviderLogo
case 'aihubmix':
return AiHubMixProviderLogo
default:
return undefined
}
@@ -82,6 +85,10 @@ export function getModelLogo(modelId: string) {
export const PROVIDER_CONFIG = {
openai: {
api: {
url: 'https://api.openai.com',
editable: true
},
websites: {
official: 'https://openai.com/',
apiKey: 'https://platform.openai.com/api-keys',
@@ -90,6 +97,10 @@ export const PROVIDER_CONFIG = {
}
},
silicon: {
api: {
url: 'https://cloud.siliconflow.cn',
editable: false
},
websites: {
official: 'https://www.siliconflow.cn/',
apiKey: 'https://cloud.siliconflow.cn/account/ak',
@@ -98,6 +109,10 @@ export const PROVIDER_CONFIG = {
}
},
deepseek: {
api: {
url: 'https://api.deepseek.com',
editable: false
},
websites: {
official: 'https://deepseek.com/',
apiKey: 'https://platform.deepseek.com/api_keys',
@@ -106,6 +121,10 @@ export const PROVIDER_CONFIG = {
}
},
yi: {
api: {
url: 'https://api.lingyiwanwu.com',
editable: false
},
websites: {
official: 'https://platform.lingyiwanwu.com/',
apiKey: 'https://platform.lingyiwanwu.com/apikeys',
@@ -114,6 +133,10 @@ export const PROVIDER_CONFIG = {
}
},
zhipu: {
api: {
url: 'https://open.bigmodel.cn/api/paas/v4/',
editable: false
},
websites: {
official: 'https://open.bigmodel.cn/',
apiKey: 'https://open.bigmodel.cn/usercenter/apikeys',
@@ -122,6 +145,10 @@ export const PROVIDER_CONFIG = {
}
},
moonshot: {
api: {
url: 'https://api.moonshot.cn',
editable: false
},
websites: {
official: 'https://moonshot.ai/',
apiKey: 'https://platform.moonshot.cn/console/api-keys',
@@ -130,6 +157,10 @@ export const PROVIDER_CONFIG = {
}
},
baichuan: {
api: {
url: 'https://api.baichuan-ai.com',
editable: false
},
websites: {
official: 'https://www.baichuan-ai.com/',
apiKey: 'https://platform.baichuan-ai.com/console/apikey',
@@ -138,6 +169,10 @@ export const PROVIDER_CONFIG = {
}
},
dashscope: {
api: {
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
editable: false
},
websites: {
official: 'https://dashscope.aliyun.com/',
apiKey: 'https://help.aliyun.com/zh/dashscope/developer-reference/acquisition-and-configuration-of-api-key',
@@ -146,6 +181,10 @@ export const PROVIDER_CONFIG = {
}
},
openrouter: {
api: {
url: 'https://openrouter.ai/api/v1/',
editable: false
},
websites: {
official: 'https://openrouter.ai/',
apiKey: 'https://openrouter.ai/settings/keys',
@@ -154,6 +193,10 @@ export const PROVIDER_CONFIG = {
}
},
groq: {
api: {
url: 'https://api.groq.com/openai',
editable: false
},
websites: {
official: 'https://groq.com/',
apiKey: 'https://console.groq.com/keys',
@@ -162,6 +205,10 @@ export const PROVIDER_CONFIG = {
}
},
ollama: {
api: {
url: 'http://localhost:11434/v1/',
editable: true
},
websites: {
official: 'https://ollama.com/',
docs: 'https://github.com/ollama/ollama/tree/main/docs',
@@ -169,11 +216,27 @@ export const PROVIDER_CONFIG = {
}
},
anthropic: {
api: {
url: 'https://api.anthropic.com/',
editable: false
},
websites: {
official: 'https://anthropic.com/',
apiKey: 'https://console.anthropic.com/settings/keys',
docs: 'https://docs.anthropic.com/en/docs',
models: 'https://docs.anthropic.com/en/docs/about-claude/models'
}
},
aihubmix: {
api: {
url: 'https://aihubmix.com',
editable: false
},
websites: {
official: 'https://aihubmix.com/',
apiKey: 'https://aihubmix.com/token',
docs: 'https://doc.aihubmix.com/',
models: 'https://aihubmix.com/models'
}
}
}

View File

@@ -9,5 +9,6 @@ declare global {
message: MessageInstance
modal: HookAPI
keyv: KeyvStorage
mermaid: any
}
}

View File

@@ -4,9 +4,11 @@ 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 useAppInitEffect() {
export function useAppInit() {
const dispatch = useAppDispatch()
const { proxyUrl } = useSettings()
useEffect(() => {
runAsyncFunction(async () => {
@@ -22,4 +24,8 @@ export function useAppInitEffect() {
isPackaged && setTimeout(window.api.checkForUpdate, 3000)
})
}, [])
useEffect(() => {
proxyUrl && window.api.setProxy(proxyUrl)
}, [proxyUrl])
}

View File

@@ -1,20 +1,21 @@
import { getDefaultTopic } from '@renderer/services/assistant'
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addTopic as _addTopic,
removeAllTopics as _removeAllTopics,
removeTopic as _removeTopic,
setModel as _setModel,
updateAssistants as _updateAssistants,
updateDefaultAssistant as _updateDefaultAssistant,
updateTopic as _updateTopic,
updateTopics as _updateTopics,
addAssistant,
addTopic,
removeAllTopics,
removeAssistant,
updateAssistant
removeTopic,
setModel,
updateAssistant,
updateAssistants,
updateAssistantSettings,
updateDefaultAssistant,
updateTopic,
updateTopics
} from '@renderer/store/assistants'
import { setDefaultModel as _setDefaultModel, setTopicNamingModel as _setTopicNamingModel } from '@renderer/store/llm'
import { Assistant, Model, Topic } from '@renderer/types'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import localforage from 'localforage'
export function useAssistants() {
@@ -23,9 +24,8 @@ export function useAssistants() {
return {
assistants,
updateAssistants: (assistants: Assistant[]) => dispatch(_updateAssistants(assistants)),
updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)),
addAssistant: (assistant: Assistant) => dispatch(addAssistant(assistant)),
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
removeAssistant: (id: string) => {
dispatch(removeAssistant({ id }))
const assistant = assistants.find((a) => a.id === id)
@@ -44,17 +44,21 @@ export function useAssistant(id: string) {
return {
assistant,
model: assistant?.model ?? defaultModel,
addTopic: (topic: Topic) => dispatch(_addTopic({ assistantId: assistant.id, topic })),
removeTopic: (topic: Topic) => dispatch(_removeTopic({ assistantId: assistant.id, topic })),
updateTopic: (topic: Topic) => dispatch(_updateTopic({ assistantId: assistant.id, topic })),
updateTopics: (topics: Topic[]) => dispatch(_updateTopics({ assistantId: assistant.id, topics })),
removeAllTopics: () => dispatch(_removeAllTopics({ assistantId: assistant.id })),
setModel: (model: Model) => dispatch(_setModel({ assistantId: assistant.id, model }))
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
removeTopic: (topic: Topic) => dispatch(removeTopic({ assistantId: assistant.id, topic })),
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
setModel: (model: Model) => dispatch(setModel({ assistantId: assistant.id, model })),
updateAssistant: (assistant: Assistant) => dispatch(updateAssistant(assistant)),
updateAssistantSettings: (settings: AssistantSettings) => {
dispatch(updateAssistantSettings({ assistantId: assistant.id, settings }))
}
}
}
export function useDefaultAssistant() {
const { defaultAssistant } = useAppSelector((state) => state.assistants)
const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant)
const dispatch = useAppDispatch()
return {
@@ -62,7 +66,7 @@ export function useDefaultAssistant() {
...defaultAssistant,
topics: [getDefaultTopic()]
},
updateDefaultAssistant: (assistant: Assistant) => dispatch(_updateDefaultAssistant({ assistant }))
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
}
}

View File

@@ -1,25 +1,31 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import {
addModel as _addModel,
removeModel as _removeModel,
updateProvider as _updateProvider,
updateProviders as _updateProviders,
addModel,
addProvider,
removeProvider
removeModel,
removeProvider,
updateProvider,
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,
(providers) => providers.filter((p) => p.enabled)
)
export function useProviders() {
const providers = useAppSelector((state) => state.llm.providers.filter((p) => p.enabled))
const providers = useAppSelector(selectEnabledProviders)
const dispatch = useAppDispatch()
return {
providers,
addProvider: (provider: Provider) => dispatch(addProvider(provider)),
removeProvider: (provider: Provider) => dispatch(removeProvider(provider)),
updateProvider: (provider: Provider) => dispatch(_updateProvider(provider)),
updateProviders: (providers: Provider[]) => dispatch(_updateProviders(providers))
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
updateProviders: (providers: Provider[]) => dispatch(updateProviders(providers))
}
}
@@ -42,9 +48,9 @@ export function useProvider(id: string) {
return {
provider,
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 }))
updateProvider: (provider: Provider) => dispatch(updateProvider(provider)),
addModel: (model: Model) => dispatch(addModel({ providerId: id, model })),
removeModel: (model: Model) => dispatch(removeModel({ providerId: id, model }))
}
}

View File

@@ -1,12 +1,28 @@
import { useAppDispatch, useAppSelector } from '@renderer/store'
import { toggleRightSidebar } from '@renderer/store/settings'
import { setShowRightSidebar, toggleRightSidebar, toggleShowAssistants } from '@renderer/store/settings'
export function useShowRightSidebar() {
const showRightSidebar = useAppSelector((state) => state.settings.showRightSidebar)
const dispatch = useAppDispatch()
return {
showRightSidebar,
setShowRightSidebar: () => dispatch(toggleRightSidebar())
rightSidebarShown: showRightSidebar,
toggleRightSidebar: () => dispatch(toggleRightSidebar()),
showRightSidebar: () => dispatch(setShowRightSidebar(true)),
hideRightSidebar: () => dispatch(setShowRightSidebar(false))
}
}
export function useShowAssistants() {
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
const dispatch = useAppDispatch()
return {
showAssistants,
toggleShowAssistants: () => dispatch(toggleShowAssistants())
}
}
export function useRuntime() {
return useAppSelector((state) => state.runtime)
}

View File

@@ -1,9 +1,13 @@
import { Assistant } from '@renderer/types'
import { Assistant, Topic } from '@renderer/types'
import { find } from 'lodash'
import { useEffect, useState } from 'react'
let _activeTopic: Topic
export function useActiveTopic(assistant: Assistant) {
const [activeTopic, setActiveTopic] = useState(assistant?.topics[0])
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
_activeTopic = activeTopic
useEffect(() => {
// activeTopic not in assistant.topics

View File

@@ -23,7 +23,9 @@ const resources = {
duplicate: 'Duplicate',
copy: 'Copy',
regenerate: 'Regenerate',
provider: 'Provider'
provider: 'Provider',
you: 'You',
save: 'Save'
},
button: {
add: 'Add',
@@ -39,17 +41,20 @@ const resources = {
'error.enter.api.key': 'Please enter your API key first',
'error.enter.api.host': 'Please enter your API host first',
'error.enter.model': 'Please select a model first',
'error.invalid.proxy.url': 'Invalid proxy URL',
'api.connection.failed': 'Connection failed',
'api.connection.success': 'Connection successful',
'chat.completion.paused': 'Chat completion paused'
'chat.completion.paused': 'Chat completion paused',
'switch.disabled': 'Switching is disabled while the assistant is generating'
},
chat: {
save: 'Save'
},
assistant: {
'default.name': 'Default Assistant',
'default.name': '😀 Default Assistant',
'default.description': "Hello, I'm Default Assistant. You can start chatting with me right away",
'default.topic.name': 'Default Topic',
'topics.title': 'Topics',
'topics.hide_topics': 'Hide Topics',
'topics.show_topics': 'Show Topics',
'topics.auto_rename': 'Auto Rename',
'topics.edit.title': 'Rename',
'topics.edit.placeholder': 'Enter new name',
@@ -64,7 +69,16 @@ const resources = {
'input.clear.content': 'Are you sure to clear all messages?',
'input.placeholder': 'Type your message here...',
'input.send': 'Send',
'input.pause': 'Pause'
'input.pause': 'Pause',
'input.settings': 'Settings',
'settings.temperature': 'Temperature',
'settings.temperature.tip':
'Lower values make the model more creative and unpredictable, while higher values make it more deterministic and precise.',
'settings.conext_count': 'Context',
'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'
},
apps: {
title: 'Agents'
@@ -81,16 +95,25 @@ const resources = {
ollama: 'Ollama',
baichuan: 'Baichuan',
dashscope: 'DashScope',
anthropic: 'Anthropic'
anthropic: 'Anthropic',
aihubmix: 'AiHubMix'
},
settings: {
title: 'Settings',
general: 'General',
general: 'General Settings',
provider: 'Model Provider',
model: 'Model Settings',
assistant: 'Default Assistant',
about: 'About',
about: 'About & Feedback',
'messages.model.title': 'Model Settings',
'messages.title': 'Message Settings',
'messages.divider': 'Show divider between messages',
'messages.use_serif_font': 'Use serif font',
'messages.input.title': 'Input Settings',
'messages.input.show_estimated_tokens': 'Show estimated input tokens',
'general.title': 'General Settings',
'general.user_name': 'User Name',
'general.user_name.placeholder': 'Enter your name',
'provider.api_key': 'API Key',
'provider.check': 'Check',
'provider.get_api_key': 'Get API Key',
@@ -98,6 +121,7 @@ const resources = {
'provider.docs_check': 'Check',
'provider.docs_more_details': 'for more details',
'provider.search_placeholder': 'Search model id or name',
'provider.api.url.reset': 'Reset',
'models.default_assistant_model': 'Default Assistant Model',
'models.topic_naming_model': 'Topic Naming Model',
'models.add.add_model': 'Add Model',
@@ -111,6 +135,7 @@ const resources = {
'models.add.group_name.placeholder': 'Optional e.g. ChatGPT',
'models.empty': 'No models found',
'assistant.title': 'Default Assistant',
'assistant.model_params': 'Model Parameters',
'about.description': 'A powerful AI assistant for producer',
'about.updateNotAvailable': 'You are using the latest version',
'about.checkingUpdate': 'Checking for updates...',
@@ -120,7 +145,17 @@ const resources = {
'provider.delete.title': 'Delete Provider',
'provider.delete.content': 'Are you sure you want to delete this provider?',
'provider.edit.name': 'Provider Name',
'provider.edit.name.placeholder': 'Example: OpenAI'
'provider.edit.name.placeholder': 'Example: OpenAI',
'about.title': 'About',
'about.releases.title': '📔 Release Notes',
'about.releases.button': 'Releases',
'about.website.title': '🌐 Official Website',
'about.website.button': 'Website',
'about.feedback.title': '📝 Feedback',
'about.feedback.button': 'Feedback',
'about.contact.title': '📧 Contact',
'about.contact.button': 'Email',
'proxy.title': 'Proxy Address'
}
}
},
@@ -144,7 +179,8 @@ const resources = {
duplicate: '复制',
copy: '复制',
regenerate: '重新生成',
provider: '提供商'
provider: '提供商',
you: '用户'
},
button: {
add: '添加',
@@ -160,17 +196,20 @@ const resources = {
'error.enter.api.key': '请输入您的 API 密钥',
'error.enter.api.host': '请输入您的 API 地址',
'error.enter.model': '请选择一个模型',
'error.invalid.proxy.url': '无效的代理地址',
'api.connection.failed': '连接失败',
'api.connection.success': '连接成功',
'chat.completion.paused': '会话已停止'
'chat.completion.paused': '会话已停止',
'switch.disabled': '模型回复完成后才能切换'
},
chat: {
save: '保存'
},
assistant: {
'default.name': '默认助手',
'default.name': '😃 默认助手 - Assistant',
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
'default.topic.name': '默认话题',
'topics.title': '话题',
'topics.hide_topics': '隐藏话题',
'topics.show_topics': '显示话题',
'topics.auto_rename': 'AI 重命名',
'topics.edit.title': '重命名',
'topics.edit.placeholder': '输入新名称',
@@ -185,7 +224,17 @@ const resources = {
'input.clear.content': '确定要清除所有消息吗?',
'input.placeholder': '在这里输入消息...',
'input.send': '发送',
'input.pause': '暂停'
'input.pause': '暂停',
'input.settings': '设置',
'settings.temperature': '模型温度',
'settings.temperature.tip':
'模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7',
'settings.conext_count': '上下文数',
'settings.conext_count.tip':
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10代码生成建议 5-10',
'settings.reset': '重置',
'settings.set_as_default': '应用到默认助手',
'settings.max': '不限'
},
apps: {
title: '智能体'
@@ -202,16 +251,25 @@ const resources = {
ollama: 'Ollama',
baichuan: '百川',
dashscope: '阿里云灵积',
anthropic: 'Anthropic'
anthropic: 'Anthropic',
aihubmix: 'AiHubMix'
},
settings: {
title: '设置',
general: '常规',
general: '常规设置',
provider: '模型提供商',
model: '模型设置',
assistant: '默认助手',
about: '关于',
about: '关于我们',
'messages.model.title': '模型设置',
'messages.title': '消息设置',
'messages.divider': '消息分割线',
'messages.use_serif_font': '使用衬线字体',
'messages.input.title': '输入设置',
'messages.input.show_estimated_tokens': '状态显示',
'general.title': '常规设置',
'general.user_name': '用户名',
'general.user_name.placeholder': '请输入用户名',
'provider.api_key': 'API 密钥',
'provider.check': '检查',
'provider.get_api_key': '点击这里获取密钥',
@@ -219,6 +277,7 @@ const resources = {
'provider.docs_check': '查看',
'provider.docs_more_details': '获取更多详情',
'provider.search_placeholder': '搜索模型 ID 或名称',
'provider.api.url.reset': '重置',
'models.default_assistant_model': '默认助手模型',
'models.topic_naming_model': '话题命名模型',
'models.add.add_model': '添加模型',
@@ -232,7 +291,8 @@ const resources = {
'models.add.group_name.placeholder': '例如 ChatGPT',
'models.empty': '没有模型',
'assistant.title': '默认助手',
'about.description': '一个为创造者而生的 AI 助手',
'assistant.model_params': '模型参数',
'about.description': '一款为创造者而生的 AI 助手',
'about.updateNotAvailable': '你的软件已是最新版本',
'about.checkingUpdate': '正在检查更新...',
'about.updateError': '更新出错',
@@ -241,7 +301,17 @@ const resources = {
'provider.delete.title': '删除提供商',
'provider.delete.content': '确定要删除此模型提供商吗?',
'provider.edit.name': '模型提供商名称',
'provider.edit.name.placeholder': '例如 OpenAI'
'provider.edit.name.placeholder': '例如 OpenAI',
'about.title': '关于我们',
'about.releases.title': '📔 更新日志',
'about.releases.button': '查看',
'about.website.title': '🌐 官方网站',
'about.website.button': '查看',
'about.feedback.title': '📝 意见反馈',
'about.feedback.button': '反馈',
'about.contact.title': '📧 邮件联系',
'about.contact.button': '邮件',
'proxy.title': '代理地址'
}
}
}

View File

@@ -1,7 +1,7 @@
import localforage from 'localforage'
import KeyvStorage from '@kangfenmao/keyv-storage'
import * as Sentry from '@sentry/electron/renderer'
import { isProduction } from './utils'
import { isProduction, loadScript } from './utils'
async function initSentry() {
if (await isProduction()) {
@@ -21,6 +21,18 @@ async function initSentry() {
}
}
export async function initMermaid() {
if (!window.mermaid) {
await loadScript('https://unpkg.com/mermaid@10.9.1/dist/mermaid.min.js')
window.mermaid.initialize({
startOnLoad: true,
theme: 'dark',
securityLevel: 'loose'
})
window.mermaid.contentLoaded()
}
}
function init() {
localforage.config({
driver: localforage.INDEXEDDB,

View File

@@ -5,17 +5,21 @@ import styled from 'styled-components'
import Chat from './components/Chat'
import Assistants from './components/Assistants'
import { uuid } from '@renderer/utils'
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { Tooltip } from 'antd'
import Navigation from './components/Navigation'
import { useTranslation } from 'react-i18next'
import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore'
import Navigation from './components/NavigationCenter'
import { isMac, isWindows } from '@renderer/config/constant'
import { Assistant } from '@renderer/types'
let _activeAssistant: Assistant
const HomePage: FC = () => {
const { assistants, addAssistant } = useAssistants()
const [activeAssistant, setActiveAssistant] = useState(assistants[0])
const { showRightSidebar, setShowRightSidebar } = useShowRightSidebar()
const [activeAssistant, setActiveAssistant] = useState(_activeAssistant || assistants[0])
const { rightSidebarShown, toggleRightSidebar } = useShowRightSidebar()
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const { defaultAssistant } = useDefaultAssistant()
const { t } = useTranslation()
_activeAssistant = activeAssistant
const onCreateAssistant = () => {
const assistant = { ...defaultAssistant, id: uuid() }
@@ -26,29 +30,31 @@ const HomePage: FC = () => {
return (
<Container>
<Navbar>
<NavbarLeft style={{ justifyContent: 'flex-end', borderRight: 'none' }}>
<NewButton onClick={onCreateAssistant}>
<i className="iconfont icon-a-addchat"></i>
</NewButton>
</NavbarLeft>
<Navigation activeAssistant={activeAssistant} />
<NavbarRight style={{ justifyContent: 'flex-end', padding: 5 }}>
<Tooltip
placement="left"
title={showRightSidebar ? t('assistant.topics.hide_topics') : t('assistant.topics.show_topics')}
arrow>
<NewButton onClick={setShowRightSidebar}>
<i className={`iconfont ${showRightSidebar ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
{showAssistants && (
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
<NewButton onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
<i className="iconfont icon-hidesidebarhoriz" />
</NewButton>
</Tooltip>
<NewButton onClick={onCreateAssistant}>
<i className="iconfont icon-a-addchat"></i>
</NewButton>
</NavbarLeft>
)}
<Navigation activeAssistant={activeAssistant} />
<NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 8 }}>
<NewButton onClick={toggleRightSidebar}>
<i className={`iconfont ${rightSidebarShown ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
</NewButton>
</NavbarRight>
</Navbar>
<ContentContainer>
<Assistants
activeAssistant={activeAssistant}
setActiveAssistant={setActiveAssistant}
onCreateAssistant={onCreateAssistant}
/>
{showAssistants && (
<Assistants
activeAssistant={activeAssistant}
setActiveAssistant={setActiveAssistant}
onCreateAssistant={onCreateAssistant}
/>
)}
<Chat assistant={activeAssistant} />
</ContentContainer>
</Container>
@@ -59,33 +65,34 @@ const Container = styled.div`
display: flex;
flex: 1;
flex-direction: column;
height: calc(100vh - var(--navbar-height));
`
const ContentContainer = styled.div`
display: flex;
flex: 1;
flex-direction: row;
height: 100%;
`
const NewButton = styled.div`
export const NewButton = styled.div`
-webkit-app-region: none;
border-radius: 4px;
width: 34px;
height: 34px;
width: 30px;
height: 30px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
color: var(--color-icon);
.iconfont {
font-size: 22px;
.icon-a-addchat {
font-size: 20px;
}
.anticon {
font-size: 19px;
}
.icon-showsidebarhoriz,
.icon-hidesidebarhoriz {
font-size: 18px;
font-size: 17px;
}
&:hover {
background-color: var(--color-background-soft);

View File

@@ -1,13 +1,16 @@
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { useAssistants } from '@renderer/hooks/useAssistant'
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
import { getDefaultTopic } from '@renderer/services/assistant'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { useAppSelector } from '@renderer/store'
import { Assistant } from '@renderer/types'
import { droppableReorder, uuid } from '@renderer/utils'
import { Dropdown, MenuProps } from 'antd'
import { Dropdown } from 'antd'
import { ItemType } from 'antd/es/menu/interface'
import { last } from 'lodash'
import { FC, useRef } from 'react'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@@ -18,52 +21,48 @@ interface Props {
}
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
const { assistants, removeAssistant, updateAssistant, addAssistant, updateAssistants } = useAssistants()
const targetAssistant = useRef<Assistant | null>(null)
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
const { updateAssistant } = useAssistant(activeAssistant.id)
const generating = useAppSelector((state) => state.runtime.generating)
const { t } = useTranslation()
const onDelete = (assistant: Assistant) => {
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant()
removeAssistant(assistant.id)
setTimeout(() => {
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant()
}, 0)
}
const items: MenuProps['items'] = [
{
label: t('common.edit'),
key: 'edit',
icon: <EditOutlined />,
async onClick() {
if (targetAssistant.current) {
const _assistant = await AssistantSettingPopup.show({ assistant: targetAssistant.current })
const getMenuItems = (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 />,
async onClick() {
const assistant: Assistant = { ...activeAssistant, id: uuid(), topics: [getDefaultTopic()] }
addAssistant(assistant)
setActiveAssistant(assistant)
}
},
{ type: 'divider' },
{
label: t('common.delete'),
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => {
targetAssistant.current && onDelete(targetAssistant.current)
}
}
]
] as ItemType[]
const onDragEnd = (result: DropResult) => {
if (result.destination) {
@@ -74,6 +73,15 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
}
}
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)
}
return (
<Container>
<DragDropContext onDragEnd={onDragEnd}>
@@ -84,15 +92,11 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
<Draggable key={`draggable_${assistant.id}_${index}`} draggableId={assistant.id} index={index}>
{(provided) => (
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<Dropdown
key={assistant.id}
menu={{ items }}
trigger={['contextMenu']}
onOpenChange={() => (targetAssistant.current = assistant)}>
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
<AssistantItem
onClick={() => setActiveAssistant(assistant)}
onClick={() => onSwitchAssistant(assistant)}
className={assistant.id === activeAssistant?.id ? 'active' : ''}>
<AssistantName>{assistant.name}</AssistantName>
<AssistantName>{assistant.name || t('assistant.default.name')}</AssistantName>
</AssistantItem>
</Dropdown>
</div>
@@ -145,7 +149,7 @@ const AssistantItem = styled.div`
const AssistantName = styled.div`
font-size: 14px;
color: var(--color-text-1);
font-weight: bold;
font-weight: 500;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;

View File

@@ -4,7 +4,7 @@ import styled from 'styled-components'
import Inputbar from './Inputbar'
import Messages from './Messages'
import { Flex } from 'antd'
import Topics from './Topics'
import RightSidebar from './RightSidebar'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useActiveTopic } from '@renderer/hooks/useTopic'
@@ -16,17 +16,13 @@ const Chat: FC<Props> = (props) => {
const { assistant } = useAssistant(props.assistant.id)
const { activeTopic, setActiveTopic } = useActiveTopic(assistant)
if (!assistant) {
return null
}
return (
<Container id="chat">
<Flex vertical flex={1} justify="space-between">
<Messages assistant={assistant} topic={activeTopic} />
<Inputbar assistant={assistant} setActiveTopic={setActiveTopic} />
</Flex>
<Topics assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
<RightSidebar assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
</Container>
)
}

View File

@@ -4,6 +4,8 @@ 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 { initMermaid } from '@renderer/init'
interface CodeBlockProps {
children: string
@@ -21,6 +23,11 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) =
window.message.success({ content: t('message.copied'), key: 'copy-code' })
}
if (match && match[1] === 'mermaid') {
initMermaid()
return <Mermaid chart={children} />
}
return match ? (
<div>
<CodeHeader>

View File

@@ -1,29 +1,30 @@
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Assistant, Message, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
import { MoreOutlined } from '@ant-design/icons'
import { Button, Popconfirm, Tooltip } from 'antd'
import { useShowRightSidebar } from '@renderer/hooks/useStore'
import { useAssistant } from '@renderer/hooks/useAssistant'
import {
ClearOutlined,
ControlOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
HistoryOutlined,
MoreOutlined,
PauseCircleOutlined,
PlusCircleOutlined
} from '@ant-design/icons'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import { isEmpty } from 'lodash'
import SendMessageSetting from './SendMessageSetting'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import dayjs from 'dayjs'
import store, { useAppSelector } from '@renderer/store'
import { getDefaultTopic } from '@renderer/services/assistant'
import { useTranslation } from 'react-i18next'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import store, { useAppSelector } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Message, Topic } from '@renderer/types'
import { estimateInputTokenCount, uuid } from '@renderer/utils'
import { Button, Popconfirm, Tooltip } from 'antd'
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
import dayjs from 'dayjs'
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'
interface Props {
assistant: Assistant
@@ -32,13 +33,12 @@ interface Props {
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
const [text, setText] = useState('')
const { setShowRightSidebar } = useShowRightSidebar()
const { addTopic } = useAssistant(assistant.id)
const { sendMessageShortcut } = useSettings()
const { sendMessageShortcut, showInputEstimatedTokens } = useSettings()
const [expended, setExpend] = useState(false)
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
const generating = useAppSelector((state) => state.runtime.generating)
const inputRef = useRef<TextAreaRef>(null)
const { t } = useTranslation()
const sendMessage = () => {
@@ -65,6 +65,8 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
setText('')
}
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (sendMessageShortcut === 'Enter' && event.key === 'Enter') {
if (event.shiftKey) {
@@ -81,7 +83,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}
const addNewTopic = useCallback(() => {
const topic: Topic = getDefaultTopic()
const topic = getDefaultTopic()
addTopic(topic)
setActiveTopic(topic)
}, [addTopic, setActiveTopic])
@@ -99,6 +101,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
if (!generating) {
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
addNewTopic()
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
inputRef.current?.focus()
}
}
@@ -108,11 +111,13 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
}, [addNewTopic, generating])
useEffect(() => {
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => {
setText(message.content)
inputRef.current?.focus()
})
}),
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, _setEstimateTokenCount)
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [])
@@ -130,11 +135,6 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<PlusCircleOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.topics')} arrow>
<ToolbarButton type="text" onClick={setShowRightSidebar}>
<HistoryOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.clear')} arrow>
<Popconfirm
icon={false}
@@ -148,6 +148,16 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
</ToolbarButton>
</Popconfirm>
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.topics')} arrow>
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)}>
<HistoryOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={t('assistant.input.settings')} arrow>
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS)}>
<ControlOutlined />
</ToolbarButton>
</Tooltip>
<Tooltip placement="top" title={expended ? t('assistant.input.collapse') : t('assistant.input.expand')} arrow>
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
@@ -158,7 +168,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
{generating && (
<Tooltip placement="top" title={t('assistant.input.pause')} arrow>
<ToolbarButton type="text" onClick={onPause}>
<PauseCircleOutlined />
<PauseCircleOutlined style={{ color: 'var(--color-error)' }} />
</ToolbarButton>
</Tooltip>
)}
@@ -177,9 +187,16 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
autoFocus
contextMenu="true"
variant="borderless"
styles={{ textarea: { paddingLeft: 0 } }}
showCount
ref={inputRef}
styles={{ textarea: { paddingLeft: 0 } }}
/>
{showInputEstimatedTokens && (
<TextCount>
<HistoryOutlined /> {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} | T
{`${inputTokenCount}/${estimateTokenCount}`}
</TextCount>
)}
</Container>
)
}
@@ -192,6 +209,7 @@ const Container = styled.div`
border-top: 0.5px solid var(--color-border);
padding: 5px 15px;
transition: all 0.3s ease;
position: relative;
`
const Textarea = styled(TextArea)`
@@ -235,4 +253,17 @@ const ToolbarButton = styled(Button)`
}
`
const TextCount = styled.div`
position: absolute;
right: 0;
bottom: 0;
font-size: 11px;
color: var(--color-text-3);
z-index: 10;
background-color: #121212;
padding: 2px 8px;
border-top-left-radius: 7px;
user-select: none;
`
export default Inputbar

View File

@@ -0,0 +1,15 @@
import React, { useEffect } from 'react'
interface Props {
chart: string
}
const Mermaid: React.FC<Props> = ({ chart }) => {
useEffect(() => {
window?.mermaid?.contentLoaded()
}, [])
return <div className="mermaid">{chart}</div>
}
export default Mermaid

View File

@@ -1,18 +1,21 @@
import { Message } from '@renderer/types'
import { Avatar, Tooltip } from 'antd'
import { FC } from 'react'
import styled from 'styled-components'
import useAvatar from '@renderer/hooks/useAvatar'
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
import Markdown from 'react-markdown'
import CodeBlock from './CodeBlock'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { getModelLogo } from '@renderer/config/provider'
import { CopyOutlined, DeleteOutlined, EditOutlined, MenuOutlined, SaveOutlined, SyncOutlined } from '@ant-design/icons'
import Logo from '@renderer/assets/images/logo.png'
import { SyncOutlined } from '@ant-design/icons'
import { getModelLogo } from '@renderer/config/provider'
import { useAssistant } from '@renderer/hooks/useAssistant'
import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
import { Message } from '@renderer/types'
import { firstLetter } from '@renderer/utils'
import { Avatar, Dropdown, Tooltip } from 'antd'
import dayjs from 'dayjs'
import { isEmpty, upperFirst } from 'lodash'
import { FC, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { isEmpty } from 'lodash'
import Markdown from 'react-markdown'
import styled from 'styled-components'
import CodeBlock from './CodeBlock'
import { useRuntime } from '@renderer/hooks/useStore'
interface Props {
message: Message
@@ -25,8 +28,12 @@ interface Props {
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
const avatar = useAvatar()
const { t } = useTranslation()
const { assistant } = useAssistant(message.assistantId)
const { userName, showMessageDivider, messageFont } = useSettings()
const { generating } = useRuntime()
const isLastMessage = index === 0
const isUserMessage = message.role === 'user'
const canRegenerate = isLastMessage && message.role === 'assistant'
const onCopy = () => {
@@ -61,18 +68,56 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
return message.content
}
const getUserName = useCallback(() => {
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])
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 messageBorder = showMessageDivider ? undefined : 'none'
return (
<MessageContainer key={message.id}>
<AvatarWrapper>
{message.role === 'assistant' ? (
<Avatar src={message.modelId ? getModelLogo(message.modelId) : Logo}>
{firstLetter(message.modelId).toUpperCase()}
</Avatar>
) : (
<Avatar src={avatar} />
)}
</AvatarWrapper>
<MessageContent>
<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()}
</Avatar>
) : (
<Avatar src={avatar} size={35} />
)}
<UserWrap>
<UserName>{getUserName()}</UserName>
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
</UserWrap>
</AvatarWrapper>
</MessageHeader>
<MessageContent style={{ fontFamily }}>
{message.status === 'sending' && (
<MessageContentLoading>
<SyncOutlined spin size={24} />
@@ -83,32 +128,43 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
{getMessageContent(message)}
</Markdown>
)}
{message.usage && !generating && (
<MessageMetadata>
Tokens: {message.usage.total_tokens} | {message.usage.prompt_tokens}{message.usage.completion_tokens}
</MessageMetadata>
)}
{showMenu && (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
{message.role === 'user' && (
<Tooltip title="Edit" mouseEnterDelay={0.8}>
<EditOutlined onClick={onEdit} />
<ActionButton>
<EditOutlined onClick={onEdit} />
</ActionButton>
</Tooltip>
)}
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
<CopyOutlined onClick={onCopy} />
<ActionButton>
<CopyOutlined onClick={onCopy} />
</ActionButton>
</Tooltip>
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
<DeleteOutlined onClick={onDelete} />
<ActionButton>
<DeleteOutlined onClick={onDelete} />
</ActionButton>
</Tooltip>
{canRegenerate && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<SyncOutlined onClick={onRegenerate} />
<ActionButton>
<SyncOutlined onClick={onRegenerate} />
</ActionButton>
</Tooltip>
)}
<MessageMetadata>{message.modelId}</MessageMetadata>
{message.usage && (
<>
<MessageMetadata style={{ textTransform: 'uppercase' }}>
tokens used: {message.usage.total_tokens} (IN:{message.usage.prompt_tokens}/OUT:
{message.usage.completion_tokens})
</MessageMetadata>
</>
{!isUserMessage && (
<Dropdown menu={{ items: getDropdownMenus(message) }} trigger={['click']} placement="topRight" arrow>
<ActionButton>
<MenuOutlined />
</ActionButton>
</Dropdown>
)}
</MenusBar>
)}
@@ -119,26 +175,21 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
const MessageContainer = styled.div`
display: flex;
flex-direction: row;
padding: 10px 15px;
position: relative;
`
const AvatarWrapper = styled.div`
margin-right: 10px;
`
const MessageContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
padding: 10px;
position: relative;
border-bottom: 0.5px dotted var(--color-border);
.menubar {
opacity: 0;
transition: opacity 0.2s ease;
&.show {
opacity: 1;
}
&.user {
position: absolute;
top: 15px;
right: 10px;
}
}
&:hover {
.menubar {
@@ -147,6 +198,47 @@ const MessageContent = styled.div`
}
`
const MessageHeader = styled.div`
margin-right: 10px;
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 4px;
justify-content: space-between;
`
const AvatarWrapper = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
const UserWrap = styled.div`
display: flex;
flex-direction: column;
justify-content: space-between;
margin-left: 12px;
`
const UserName = styled.div`
font-size: 14px;
font-weight: 600;
`
const MessageTime = styled.div`
font-size: 12px;
color: var(--color-text-3);
`
const MessageContent = styled.div`
display: flex;
flex: 1;
flex-direction: column;
justify-content: space-between;
margin-left: 46px;
margin-top: 5px;
`
const MessageContentLoading = styled.div`
display: flex;
flex-direction: row;
@@ -157,17 +249,9 @@ const MessageContentLoading = styled.div`
const MenusBar = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
justify-content: flex-end;
align-items: center;
gap: 6px;
.anticon {
cursor: pointer;
margin-right: 8px;
font-size: 15px;
color: var(--color-icon);
&:hover {
color: var(--color-text-1);
}
}
`
const MessageMetadata = styled.div`
@@ -176,4 +260,24 @@ const MessageMetadata = styled.div`
user-select: text;
`
const ActionButton = styled.div`
cursor: pointer;
border: 1px solid var(--color-border);
border-radius: 8px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
width: 30px;
height: 30px;
.anticon {
cursor: pointer;
font-size: 14px;
color: var(--color-icon);
}
&:hover {
color: var(--color-text-1);
}
`
export default MessageItem

View File

@@ -7,7 +7,7 @@ import MessageItem from './Message'
import { reverse } from 'lodash'
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { runAsyncFunction } from '@renderer/utils'
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
import LocalStorage from '@renderer/services/storage'
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
import { t } from 'i18next'
@@ -22,7 +22,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
const [lastMessage, setLastMessage] = useState<Message | null>(null)
const { updateTopic } = useAssistant(assistant.id)
const provider = useProviderByAssistant(assistant)
const messagesRef = useRef<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const assistantDefaultMessage: Message = {
id: 'assistant',
@@ -65,7 +65,6 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
console.debug({ assistant, provider, message: msg, topic })
onSendMessage(msg)
fetchChatCompletion({ assistant, messages: [...messages, msg], topic, onResponse: setLastMessage })
}),
@@ -95,11 +94,15 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
}, [topic.id])
useEffect(() => {
messagesRef.current?.scrollTo({ top: 100000, behavior: 'auto' })
containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' })
}, [messages])
useEffect(() => {
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
}, [assistant, messages])
return (
<Container id="messages" key={assistant.id} ref={messagesRef}>
<Container id="messages" key={assistant.id} ref={containerRef}>
{lastMessage && <MessageItem message={lastMessage} />}
{reverse([...messages]).map((message, index) => (
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
@@ -116,7 +119,10 @@ const Container = styled.div`
flex-direction: column-reverse;
max-height: calc(100vh - var(--input-bar-height) - var(--navbar-height));
padding-top: 10px;
padding-bottom: 20px;
padding-bottom: 10px;
.message:first-child {
border: none;
}
`
export default Messages

View File

@@ -1,22 +1,28 @@
import { CodeSandboxOutlined } from '@ant-design/icons'
import { NavbarCenter } from '@renderer/components/app/Navbar'
import { colorPrimary } from '@renderer/config/antd'
import { isMac } from '@renderer/config/constant'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { useProviders } from '@renderer/hooks/useProvider'
import { useShowAssistants } from '@renderer/hooks/useStore'
import { Assistant } from '@renderer/types'
import { Button, Dropdown, MenuProps } from 'antd'
import { upperFirst } from 'lodash'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { NewButton } from '../HomePage'
interface Props {
activeAssistant: Assistant
}
const Navigation: FC<Props> = ({ activeAssistant }) => {
const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
const { assistant } = useAssistant(activeAssistant.id)
const { model, setModel } = useAssistant(activeAssistant.id)
const { providers } = useProviders()
const { t } = useTranslation()
const { showAssistants, toggleShowAssistants } = useShowAssistants()
const items: MenuProps['items'] = providers
.filter((p) => p.models.length > 0)
@@ -26,19 +32,25 @@ const Navigation: FC<Props> = ({ activeAssistant }) => {
type: 'group',
children: p.models.map((m) => ({
key: m.id,
label: m.name,
label: upperFirst(m.name),
style: m.id === model?.id ? { color: colorPrimary } : undefined,
onClick: () => setModel(m)
}))
}))
return (
<NavbarCenter style={{ border: 'none', padding: '0 15px' }}>
{assistant?.name}
<NavbarCenter style={{ paddingLeft: isMac ? 16 : 8 }}>
{!showAssistants && (
<NewButton onClick={toggleShowAssistants} style={{ marginRight: 8 }}>
<i className="iconfont icon-showsidebarhoriz" />
</NewButton>
)}
<AssistantName>{assistant?.name || t('assistant.default.name')}</AssistantName>
<DropdownMenu menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }} trigger={['click']}>
<Button size="small" type="primary" ghost style={{ fontSize: '11px' }}>
{model ? model.name : t('button.select_model')}
</Button>
<DropdownButton size="small" type="primary" ghost>
<CodeSandboxOutlined />
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
</DropdownButton>
</DropdownMenu>
</NavbarCenter>
)
@@ -49,4 +61,19 @@ const DropdownMenu = styled(Dropdown)`
margin-left: 10px;
`
export default Navigation
const AssistantName = styled.span`
font-weight: bold;
margin-left: 5px;
`
const DropdownButton = styled(Button)`
font-size: 10px;
border-radius: 15px;
padding: 0 8px;
`
const ModelName = styled.span`
margin-left: -2px;
`
export default NavigationCenter

View File

@@ -0,0 +1,101 @@
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'
interface Props {
assistant: Assistant
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
}
const RightSidebar: FC<Props> = (props) => {
const [tab, setTab] = useState<'topic' | 'settings'>('topic')
const { rightSidebarShown, showRightSidebar, hideRightSidebar } = useShowRightSidebar()
const { t } = useTranslation()
const isTopicTab = tab === 'topic'
const isSettingsTab = tab === 'settings'
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => {
if (rightSidebarShown && isTopicTab) {
return hideRightSidebar()
}
if (rightSidebarShown) {
return setTab('topic')
}
showRightSidebar()
setTab('topic')
}),
EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => {
if (rightSidebarShown && isSettingsTab) {
return hideRightSidebar()
}
if (rightSidebarShown) {
return setTab('settings')
}
showRightSidebar()
setTab('settings')
}),
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => setTab('topic'))
]
return () => unsubscribes.forEach((unsub) => unsub())
}, [hideRightSidebar, isSettingsTab, isTopicTab, rightSidebarShown, showRightSidebar])
return (
<Container style={{ display: rightSidebarShown ? 'block' : 'none' }}>
<Tabs>
<Tab className={tab === 'topic' ? 'active' : ''} onClick={() => setTab('topic')}>
{t('common.topics')}
</Tab>
<Tab className={tab === 'settings' ? 'active' : ''} onClick={() => setTab('settings')}>
{t('settings.title')}
</Tab>
</Tabs>
{tab === 'topic' && <TopicsTab {...props} />}
{tab === 'settings' && <SettingsTab assistant={props.assistant} />}
</Container>
)
}
const Container = styled.div`
width: var(--topic-list-width);
height: 100%;
border-left: 0.5px solid var(--color-border);
overflow-y: auto;
.collapsed {
width: 0;
border-left: none;
}
`
const Tabs = styled.div`
display: flex;
flex-direction: row;
border-bottom: 0.5px solid var(--color-border);
padding: 0 10px;
`
const Tab = styled.div`
padding: 8px 0;
font-weight: 500;
display: flex;
flex: 1;
justify-content: center;
align-items: center;
font-size: 13px;
cursor: pointer;
color: #8a8a8a;
border-bottom: 1px solid transparent;
&.active {
color: #bbb;
font-weight: 600;
}
`
export default RightSidebar

View File

@@ -28,7 +28,7 @@ const SendMessageSetting: FC<Props> = ({ children }) => {
return (
<Dropdown
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
placement="top"
placement="topRight"
trigger={['click']}
arrow>
{children}

View File

@@ -0,0 +1,210 @@
import { Assistant } from '@renderer/types'
import styled from 'styled-components'
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 { 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'
interface Props {
assistant: Assistant
}
const SettingsTab: FC<Props> = (props) => {
const { assistant, updateAssistantSettings, updateAssistant } = useAssistant(props.assistant.id)
const [temperature, setTemperature] = useState(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
const { t } = useTranslation()
const dispatch = useAppDispatch()
const { showMessageDivider, messageFont, showInputEstimatedTokens } = useSettings()
const onUpdateAssistantSettings = useCallback(
debounce(
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
updateAssistantSettings({
...assistant.settings,
temperature: _temperature ?? temperature,
contextCount: _contextCount ?? contextCount
})
},
1000,
{
leading: false,
trailing: true
}
),
[]
)
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
setTemperature(value)
onUpdateAssistantSettings({ _temperature: value })
}
}
const onConextCountChange = (value) => {
if (!isNaN(value as number)) {
setConextCount(value)
onUpdateAssistantSettings({ _contextCount: value })
}
}
const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE)
setConextCount(DEFAULT_CONEXTCOUNT)
updateAssistant({
...assistant,
settings: {
...assistant.settings,
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT
}
})
}
useEffect(() => {
setTemperature(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
setConextCount(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
}, [assistant])
return (
<Container>
<SettingSubtitle style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
{t('settings.messages.model.title')}{' '}
<Button size="small" onClick={onReset}>
{t('assistant.settings.reset')}
</Button>
</SettingSubtitle>
<SettingDivider />
<Row align="middle">
<Label>{t('assistant.settings.conext_count')}</Label>
<Tooltip title={t('assistant.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={18}>
<Slider
min={0}
max={1.2}
onChange={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
marks={{ 0: '0', 0.7: '0.7', 1.2: '1.2' }}
step={0.1}
/>
</Col>
<Col span={6}>
<InputNumberic
min={0}
max={1.2}
step={0.1}
value={temperature}
onChange={onTemperatureChange}
controls={false}
/>
</Col>
</Row>
<Row align="middle">
<Label>{t('assistant.settings.conext_count')}</Label>
<Tooltip title={t('assistant.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" gutter={10}>
<Col span={18}>
<Slider
min={0}
max={20}
marks={{ 0: '0', 10: '10', 20: t('assistant.settings.max') }}
onChange={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
<Col span={6}>
<InputNumberic
min={0}
max={20}
step={1}
value={contextCount}
onChange={onConextCountChange}
controls={false}
/>
</Col>
</Row>
<SettingSubtitle>{t('settings.messages.title')}</SettingSubtitle>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.divider')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showMessageDivider}
onChange={(checked) => dispatch(setShowMessageDivider(checked))}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
<Switch
size="small"
checked={messageFont === 'serif'}
onChange={(checked) => dispatch(setMessageFont(checked ? 'serif' : 'system'))}
/>
</SettingRow>
<SettingDivider />
<SettingSubtitle style={{ marginTop: 20 }}>{t('settings.messages.input.title')}</SettingSubtitle>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('settings.messages.input.show_estimated_tokens')}</SettingRowTitleSmall>
<Switch
size="small"
checked={showInputEstimatedTokens}
onChange={(checked) => dispatch(setShowInputEstimatedTokens(checked))}
/>
</SettingRow>
<SettingDivider />
</Container>
)
}
const Container = styled.div`
padding: 0 15px;
`
const InputNumberic = styled(InputNumber)`
width: 45px;
padding: 0;
margin-left: 5px;
text-align: center;
.ant-input-number-input {
text-align: center;
}
`
const Label = styled.p`
margin: 0;
font-size: 12px;
font-weight: bold;
margin-right: 8px;
`
const QuestionIcon = styled(QuestionCircleOutlined)`
font-size: 12px;
cursor: pointer;
color: var(--color-text-3);
`
const SettingRowTitleSmall = styled(SettingRowTitle)`
font-size: 13px;
`
export default SettingsTab

View File

@@ -1,194 +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 { Button, Dropdown, MenuProps, Popconfirm } from 'antd'
import { FC, useRef } 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'
interface Props {
assistant: Assistant
activeTopic: Topic
setActiveTopic: (topic: Topic) => void
}
const Topics: FC<Props> = ({ assistant, activeTopic, setActiveTopic }) => {
const { showRightSidebar } = useShowRightSidebar()
const { removeTopic, updateTopic, removeAllTopics, updateTopics } = useAssistant(assistant.id)
const currentTopic = useRef<Topic | null>(null)
const { t } = useTranslation()
const topicMenuItems: MenuProps['items'] = [
{
label: t('assistant.topics.auto_rename'),
key: 'auto-rename',
icon: <SignatureOutlined />,
async onClick() {
if (currentTopic.current) {
const messages = await LocalStorage.getTopicMessages(currentTopic.current.id)
if (messages.length >= 2) {
const summaryText = await fetchMessagesSummary({ messages, assistant })
if (summaryText) {
updateTopic({ ...currentTopic.current, 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: currentTopic.current?.name || ''
})
if (name && currentTopic.current && currentTopic.current?.name !== name) {
updateTopic({ ...currentTopic.current, name })
}
}
}
]
if (assistant.topics.length > 1) {
topicMenuItems.push({ type: 'divider' })
topicMenuItems.push({
label: t('common.delete'),
danger: true,
key: 'delete',
icon: <DeleteOutlined />,
onClick() {
if (assistant.topics.length === 1) return
currentTopic.current && removeTopic(currentTopic.current)
currentTopic.current = null
setActiveTopic(assistant.topics[0])
}
})
}
const onDragEnd = (result: DropResult) => {
if (result.destination) {
const sourceIndex = result.source.index
const destIndex = result.destination.index
updateTopics(droppableReorder(assistant.topics, sourceIndex, destIndex))
}
}
if (!showRightSidebar) {
return null
}
return (
<Container className={showRightSidebar ? '' : 'collapsed'}>
<TopicTitle>
<span>
{t('assistant.topics.title')} ({assistant.topics.length})
</span>
<Popconfirm
icon={false}
title={t('assistant.topics.delete.all.title')}
description={t('assistant.topics.delete.all.content')}
placement="leftBottom"
onConfirm={removeAllTopics}
okType="danger">
<DeleteButton type="text">
<DeleteIcon />
</DeleteButton>
</Popconfirm>
</TopicTitle>
<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: topicMenuItems }}
trigger={['contextMenu']}
key={topic.id}
onOpenChange={(open) => open && (currentTopic.current = topic)}>
<TopicListItem
className={topic.id === activeTopic?.id ? 'active' : ''}
onClick={() => setActiveTopic(topic)}>
{topic.name}
</TopicListItem>
</Dropdown>
</div>
)}
</Draggable>
))}
</div>
)}
</Droppable>
</DragDropContext>
</Container>
)
}
const Container = styled.div`
width: var(--topic-list-width);
height: 100%;
border-left: 0.5px solid var(--color-border);
padding: 10px;
overflow-y: auto;
&.collapsed {
width: 0;
border-left: none;
}
`
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);
}
`
const TopicTitle = styled.div`
font-weight: bold;
margin-bottom: 10px;
font-size: 14px;
color: var(--color-text-1);
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`
const DeleteButton = styled(Button)`
width: 30px;
height: 30px;
border-radius: 50%;
padding: 0;
&:hover {
.anticon {
color: #ff4d4f;
}
}
`
const DeleteIcon = styled(DeleteOutlined)`
font-size: 16px;
`
export default Topics

View File

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

View File

@@ -1,12 +1,12 @@
import { Avatar, Button, Progress } from 'antd'
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 Changelog from './components/Changelog'
import { debounce } from 'lodash'
import { ProgressInfo } from 'electron-updater'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
const AboutSettings: FC = () => {
const [version, setVersion] = useState('')
@@ -26,8 +26,17 @@ const AboutSettings: FC = () => {
{ leading: true, trailing: false }
)
const onOpenWebsite = (suffix = '') => {
window.api.openWebsite('https://github.com/kangfenmao/cherry-studio' + suffix)
const onOpenWebsite = (url: string) => {
window.api.openWebsite(url)
}
const mailto = async () => {
const email = 'kangfenmao@qq.com'
const subject = 'Cherry Studio Feedback'
const version = (await window.api.getAppInfo()).version
const platform = window.electron.process.platform
const url = `mailto:${email}?subject=${subject}&body=%0A%0AVersion: ${version} | Platform: ${platform}`
onOpenWebsite(url)
}
useEffect(() => {
@@ -69,57 +78,92 @@ const AboutSettings: FC = () => {
}, [t])
return (
<Container>
<AvatarWrapper onClick={() => onOpenWebsite()}>
{percent > 0 && (
<ProgressCircle
type="circle"
size={104}
percent={percent}
showInfo={false}
strokeLinecap="butt"
strokeColor="#67ad5b"
/>
)}
<Avatar src={Logo} size={100} style={{ marginTop: 50, minHeight: 100 }} />
</AvatarWrapper>
<Title>
Cherry Studio <Version onClick={() => onOpenWebsite('/releases')}>(v{version})</Version>
</Title>
<Description>{t('settings.about.description')}</Description>
<CheckUpdateButton onClick={onCheckUpdate} loading={checkUpdateLoading}>
{downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')}
</CheckUpdateButton>
<Changelog />
</Container>
<SettingContainer>
<SettingTitle>{t('settings.about.title')}</SettingTitle>
<SettingDivider />
<AboutHeader>
<Row align="middle">
<AvatarWrapper onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio')}>
{percent > 0 && (
<ProgressCircle
type="circle"
size={84}
percent={percent}
showInfo={false}
strokeLinecap="butt"
strokeColor="#67ad5b"
/>
)}
<Avatar src={Logo} size={80} style={{ minHeight: 80 }} />
</AvatarWrapper>
<VersionWrapper>
<Title>Cherry Studio</Title>
<Description>{t('settings.about.description')}</Description>
<Tag
onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}
color="cyan"
style={{ marginTop: 8, cursor: 'pointer' }}>
v{version}
</Tag>
</VersionWrapper>
</Row>
<CheckUpdateButton onClick={onCheckUpdate} loading={checkUpdateLoading}>
{downloading ? t('settings.about.downloading') : t('settings.about.checkUpdate')}
</CheckUpdateButton>
</AboutHeader>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.releases.title')}</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}>
{t('settings.about.releases.button')}
</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.website.title')}</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://easys.run/cherry-studio')}>
{t('settings.about.website.button')}
</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.feedback.title')}</SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/issues')}>
{t('settings.about.feedback.button')}
</Button>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.about.contact.title')}</SettingRowTitle>
<Button onClick={mailto}>{t('settings.about.contact.button')}</Button>
</SettingRow>
<SettingDivider />
</SettingContainer>
)
}
const Container = styled.div`
const AboutHeader = styled.div`
display: flex;
flex: 1;
flex-direction: column;
flex-direction: row;
align-items: center;
justify-content: flex-start;
height: calc(100vh - var(--navbar-height));
overflow-y: scroll;
padding: 0;
padding-bottom: 50px;
justify-content: space-between;
width: 100%;
padding: 5px 0;
`
const VersionWrapper = styled.div`
display: flex;
flex-direction: column;
min-height: 80px;
justify-content: center;
align-items: flex-start;
`
const Title = styled.div`
font-size: 20px;
font-weight: bold;
color: var(--color-text-1);
margin: 10px 0;
`
const Version = styled.span`
font-size: 14px;
color: var(--color-text-2);
margin: 10px 0;
text-align: center;
cursor: pointer;
margin-bottom: 5px;
`
const Description = styled.div`
@@ -128,18 +172,17 @@ const Description = styled.div`
text-align: center;
`
const CheckUpdateButton = styled(Button)`
margin-top: 10px;
`
const CheckUpdateButton = styled(Button)``
const AvatarWrapper = styled.div`
position: relative;
cursor: pointer;
margin-right: 15px;
`
const ProgressCircle = styled(Progress)`
position: absolute;
top: 48px;
top: -2px;
left: -2px;
`

View File

@@ -1,15 +1,66 @@
import { FC } from 'react'
import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from './components'
import { Input } from 'antd'
import TextArea from 'antd/es/input/TextArea'
import { QuestionCircleOutlined } from '@ant-design/icons'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { Button, Col, Input, InputNumber, Row, Slider, Tooltip } from 'antd'
import TextArea from 'antd/es/input/TextArea'
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()
const [temperature, setTemperature] = useState(defaultAssistant.settings?.temperature ?? DEFAULT_TEMPERATURE)
const [contextCount, setConextCount] = useState(defaultAssistant.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
const { t } = useTranslation()
const onUpdateAssistantSettings = useCallback(
debounce(
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
updateDefaultAssistant({
...defaultAssistant,
settings: {
...defaultAssistant.settings,
temperature: _temperature ?? temperature,
contextCount: _contextCount ?? contextCount
}
})
},
1000,
{ leading: false, trailing: true }
),
[]
)
const onTemperatureChange = (value) => {
if (!isNaN(value as number)) {
setTemperature(value)
onUpdateAssistantSettings({ _temperature: value })
}
}
const onConextCountChange = (value) => {
if (!isNaN(value as number)) {
setConextCount(value)
onUpdateAssistantSettings({ _contextCount: value })
}
}
const onReset = () => {
setTemperature(DEFAULT_TEMPERATURE)
setConextCount(DEFAULT_CONEXTCOUNT)
updateDefaultAssistant({
...defaultAssistant,
settings: {
...defaultAssistant.settings,
temperature: DEFAULT_TEMPERATURE,
contextCount: DEFAULT_CONEXTCOUNT
}
})
}
return (
<SettingContainer>
<SettingTitle>{t('settings.assistant.title')}</SettingTitle>
@@ -27,8 +78,82 @@ const AssistantSettings: FC = () => {
value={defaultAssistant.prompt}
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })}
/>
<SettingDivider />
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.assistant.model_params')}</SettingSubtitle>
<Row align="middle">
<Label>{t('assistant.settings.temperature')}</Label>
<Tooltip title={t('assistant.settings.temperature.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
<Col span={22}>
<Slider
min={0}
max={1.2}
onChange={onTemperatureChange}
value={typeof temperature === 'number' ? temperature : 0}
marks={{ 0: '0', 0.7: '0.7', 1: '1', 1.2: '1.2' }}
step={0.1}
/>
</Col>
<Col span={2}>
<InputNumber
min={0}
max={1.2}
step={0.1}
value={temperature}
onChange={onTemperatureChange}
style={{ width: '100%' }}
/>
</Col>
</Row>
<Row align="middle">
<Label>{t('assistant.settings.conext_count')}</Label>
<Tooltip title={t('assistant.settings.conext_count.tip')}>
<QuestionIcon />
</Tooltip>
</Row>
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
<Col span={22}>
<Slider
min={0}
max={20}
marks={{ 0: '0', 5: '5', 10: '10', 15: '15', 20: t('assistant.settings.max') }}
onChange={onConextCountChange}
value={typeof contextCount === 'number' ? contextCount : 0}
step={1}
/>
</Col>
<Col span={2}>
<InputNumber
min={0}
max={20}
step={1}
value={contextCount}
onChange={onConextCountChange}
style={{ width: '100%' }}
/>
</Col>
</Row>
<Button onClick={onReset} style={{ width: 100 }}>
{t('assistant.settings.reset')}
</Button>
</SettingContainer>
)
}
const Label = styled.p`
margin: 0;
font-size: 14px;
font-weight: bold;
margin-right: 5px;
`
const QuestionIcon = styled(QuestionCircleOutlined)`
font-size: 14px;
cursor: pointer;
color: var(--color-text-3);
`
export default AssistantSettings

View File

@@ -1,20 +1,22 @@
import { FC } from 'react'
import { FC, useState } from 'react'
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
import { Avatar, Select, Upload } from 'antd'
import { Avatar, Input, Select, Upload } from 'antd'
import styled from 'styled-components'
import LocalStorage from '@renderer/services/storage'
import { compressImage } from '@renderer/utils'
import { compressImage, isValidProxyUrl } from '@renderer/utils'
import useAvatar from '@renderer/hooks/useAvatar'
import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
import { useSettings } from '@renderer/hooks/useSettings'
import { setLanguage } from '@renderer/store/settings'
import { setLanguage, setUserName } from '@renderer/store/settings'
import { useTranslation } from 'react-i18next'
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
import i18n from '@renderer/i18n'
const GeneralSettings: FC = () => {
const avatar = useAvatar()
const { language } = useSettings()
const { language, proxyUrl: storeProxyUrl, userName } = useSettings()
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
const dispatch = useAppDispatch()
const { t } = useTranslation()
@@ -24,6 +26,16 @@ const GeneralSettings: FC = () => {
localStorage.setItem('language', value)
}
const onSetProxyUrl = () => {
if (proxyUrl && !isValidProxyUrl(proxyUrl)) {
window.message.error({ content: t('message.error.invalid.proxy.url'), key: 'proxy-error' })
return
}
dispatch(_setProxyUrl(proxyUrl))
window.api.setProxy(proxyUrl)
}
return (
<SettingContainer>
<SettingTitle>{t('settings.general.title')}</SettingTitle>
@@ -62,6 +74,29 @@ const GeneralSettings: FC = () => {
</Upload>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.user_name')}</SettingRowTitle>
<Input
placeholder={t('settings.general.user_name.placeholder')}
value={userName}
onChange={(e) => dispatch(setUserName(e.target.value))}
style={{ width: 150 }}
maxLength={30}
/>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
<Input
placeholder="socks5://127.0.0.1:6153"
value={proxyUrl}
onChange={(e) => setProxyUrl(e.target.value)}
style={{ width: 300 }}
onBlur={() => onSetProxyUrl()}
type="url"
/>
</SettingRow>
<SettingDivider />
</SettingContainer>
)
}

View File

@@ -100,11 +100,11 @@ const ProviderSettings: FC = () => {
key={JSON.stringify(provider)}
className={provider.id === selectedProvider?.id ? 'active' : ''}
onClick={() => setSelectedProvider(provider)}>
{provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={24} />}
{provider.isSystem && <Avatar src={getProviderLogo(provider.id)} size={28} />}
{!provider.isSystem && (
<Avatar
size={24}
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 24 }}>
size={28}
style={{ backgroundColor: generateColorFromChar(provider.name), minWidth: 28 }}>
{getFirstCharacter(provider.name)}
</Avatar>
)}
@@ -151,7 +151,7 @@ const ProviderListContainer = styled.div`
width: var(--assistants-width);
height: calc(100vh - var(--navbar-height));
border-right: 0.5px solid var(--color-border);
padding: 10px;
padding: 10px 8px;
overflow-y: auto;
`
@@ -165,7 +165,7 @@ const ProviderListItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding: 6px 10px;
padding: 5px 8px;
margin-bottom: 5px;
width: 100%;
cursor: pointer;

View File

@@ -1,29 +0,0 @@
import changelogEn from '@renderer/CHANGELOG.en.md?raw'
import changelogZh from '@renderer/CHANGELOG.zh.md?raw'
import { FC } from 'react'
import Markdown from 'react-markdown'
import styled from 'styled-components'
import styles from './changelog.module.scss'
import i18n from '@renderer/i18n'
const Changelog: FC = () => {
const language = i18n.language
const changelog = language === 'zh-CN' ? changelogZh : changelogEn
return (
<Container>
<Markdown className={styles.markdown}>{changelog}</Markdown>
</Container>
)
}
const Container = styled.div`
font-size: 14px;
background-color: var(--color-background-soft);
margin-top: 40px;
padding: 20px;
border-radius: 5px;
width: 650px;
`
export default Changelog

View File

@@ -6,7 +6,14 @@ import { useProvider } from '@renderer/hooks/useProvider'
import { groupBy } from 'lodash'
import { SettingContainer, SettingSubtitle, SettingTitle } from '.'
import { getModelLogo } from '@renderer/config/provider'
import { CheckOutlined, EditOutlined, ExportOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons'
import {
CheckOutlined,
EditOutlined,
ExportOutlined,
LoadingOutlined,
MinusCircleOutlined,
PlusOutlined
} from '@ant-design/icons'
import AddModelPopup from './AddModelPopup'
import EditModelsPopup from './EditModelsPopup'
import Link from 'antd/es/typography/Link'
@@ -24,7 +31,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
const [apiHost, setApiHost] = useState(provider.apiHost)
const [apiValid, setApiValid] = useState(false)
const [apiChecking, setApiChecking] = useState(false)
const { updateProvider, models } = useProvider(provider.id)
const { updateProvider, models, removeModel } = useProvider(provider.id)
const { t } = useTranslation()
const modelGroups = groupBy(models, 'group')
@@ -52,8 +59,13 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
const apiKeyWebsite = providerConfig?.websites?.apiKey
const docsWebsite = providerConfig?.websites?.docs
const modelsWebsite = providerConfig?.websites?.models
const configedApiHost = providerConfig?.api?.url
const apiEditable = provider.isSystem ? providerConfig?.api?.editable : true
const apiKeyDisabled = provider.id === 'ollama'
const onReset = () => {
setApiHost(configedApiHost)
updateProvider({ ...provider, apiHost: configedApiHost })
}
return (
<SettingContainer>
@@ -81,15 +93,12 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
onChange={(e) => setApiKey(e.target.value)}
onBlur={onUpdateApiKey}
spellCheck={false}
disabled={apiKeyDisabled}
type="password"
autoFocus={provider.enabled && apiKey === ''}
/>
{!apiKeyDisabled && (
<Button type={apiValid ? 'primary' : 'default'} ghost={apiValid} onClick={onCheckApi}>
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.provider.check')}
</Button>
)}
<Button type={apiValid ? 'primary' : 'default'} ghost={apiValid} onClick={onCheckApi}>
{apiChecking ? <LoadingOutlined spin /> : apiValid ? <CheckOutlined /> : t('settings.provider.check')}
</Button>
</Space.Compact>
{apiKeyWebsite && (
<HelpTextRow>
@@ -99,22 +108,28 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
</HelpTextRow>
)}
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
<Input
value={apiHost}
placeholder={t('settings.provider.api_host')}
disabled={provider.isSystem}
onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost}
/>
<Space.Compact style={{ width: '100%' }}>
<Input
value={apiHost}
placeholder={t('settings.provider.api_host')}
onChange={(e) => setApiHost(e.target.value)}
onBlur={onUpdateApiHost}
disabled={!apiEditable}
/>
{apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>}
</Space.Compact>
<SettingSubtitle>{t('common.models')}</SettingSubtitle>
{Object.keys(modelGroups).map((group) => (
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
{modelGroups[group].map((model) => (
<ModelListItem key={model.id}>
<Avatar src={getModelLogo(model.id)} size={22} style={{ marginRight: '8px' }}>
{model.name[0].toUpperCase()}
</Avatar>
{model.name}
<ModelListHeader>
<Avatar src={getModelLogo(model.id)} size={22} style={{ marginRight: '8px' }}>
{model.name[0].toUpperCase()}
</Avatar>
{model.name}
</ModelListHeader>
<RemoveIcon onClick={() => removeModel(model)} />
</ModelListItem>
))}
</Card>
@@ -149,10 +164,16 @@ const ModelListItem = styled.div`
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
justify-content: space-between;
padding: 5px 0;
`
const ModelListHeader = styled.div`
display: flex;
flex-direction: row;
align-items: center;
`
const HelpTextRow = styled.div`
display: flex;
flex-direction: row;
@@ -170,4 +191,15 @@ const HelpLink = styled(Link)`
padding: 0 5px;
`
const RemoveIcon = styled(MinusCircleOutlined)`
margin-left: 10px;
color: var(--color-error);
cursor: pointer;
transition: all 0.2s ease-in-out;
opacity: 0.75;
&:hover {
opacity: 1;
}
`
export default ProviderSetting

View File

@@ -1,79 +0,0 @@
$background-color: #121212;
$text-color: #ffffff;
$heading-color: #00b96b;
$link-color: #3498db;
$code-background: #1e1e1e;
$code-color: #f0e7db;
.markdown {
body {
background-color: $background-color;
color: $text-color;
font-family: Arial, sans-serif;
padding: 20px;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: $heading-color;
}
h1 {
font-size: 22px;
font-weight: 700;
}
h3 {
margin: 10px 0;
font-weight: 500;
font-family: Arial, sans-serif;
}
a {
color: $link-color;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
strong {
font-weight: bold;
}
pre {
background-color: $code-background;
padding: 10px;
border-radius: 5px;
overflow-x: auto;
}
code {
background-color: $code-background;
color: $code-color;
padding: 2px 4px;
border-radius: 3px;
}
blockquote {
border-left: 4px solid $heading-color;
padding-left: 10px;
margin-left: 0;
color: #b3b3b3;
}
ul,
ol {
padding-left: 20px;
list-style: disc;
}
li {
margin-bottom: 5px;
}
}

View File

@@ -43,5 +43,6 @@ export const SettingRow = styled.div`
export const SettingRowTitle = styled.div`
font-size: 14px;
line-height: 18px;
color: var(--color-text-1);
`

View File

@@ -6,7 +6,7 @@ import { ChatCompletionCreateParamsNonStreaming, ChatCompletionMessageParam } fr
import { sum, takeRight } from 'lodash'
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
import { EVENT_NAMES } from './event'
import { removeQuotes } from '@renderer/utils'
import { getAssistantSettings, removeQuotes } from '@renderer/utils'
export default class ProviderSDK {
provider: Provider
@@ -32,10 +32,11 @@ export default class ProviderSDK {
) {
const defaultModel = getDefaultModel()
const model = assistant.model || defaultModel
const { contextCount } = getAssistantSettings(assistant)
const systemMessage = assistant.prompt ? { role: 'system', content: assistant.prompt } : undefined
const userMessages = takeRight(messages, 5).map((message) => ({
const userMessages = takeRight(messages, contextCount + 1).map((message) => ({
role: message.role,
content: message.content
}))
@@ -43,9 +44,10 @@ export default class ProviderSDK {
if (this.isAnthropic) {
await this.anthropicSdk.messages
.stream({
max_tokens: 2048,
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as MessageParam[],
model: model.id
max_tokens: 4096,
temperature: assistant?.settings?.temperature
})
.on('text', (text) => onChunk({ text: text || '' }))
.on('finalMessage', (message) =>
@@ -61,7 +63,8 @@ export default class ProviderSDK {
const stream = await this.openaiSdk.chat.completions.create({
model: model.id,
messages: [systemMessage, ...userMessages].filter(Boolean) as ChatCompletionMessageParam[],
stream: true
stream: true,
temperature: assistant?.settings?.temperature
})
for await (const chunk of stream) {
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) break
@@ -80,7 +83,7 @@ export default class ProviderSDK {
const systemMessage = {
role: 'system',
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要标点符号'
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
}
if (this.isAnthropic) {

View File

@@ -2,7 +2,7 @@ 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 { getErrorMessage, uuid } from '@renderer/utils'
import { uuid } from '@renderer/utils'
import dayjs from 'dayjs'
import { getAssistantProvider, getDefaultModel, getProviderByModel, getTopNamingModel } from './assistant'
import { EVENT_NAMES, EventEmitter } from './event'
@@ -66,8 +66,18 @@ export async function fetchChatCompletion({
export async function fetchMessagesSummary({ messages, assistant }: { messages: Message[]; assistant: Assistant }) {
const model = getTopNamingModel() || assistant.model || getDefaultModel()
const provider = getProviderByModel(model)
if (!provider.apiKey) {
return null
}
const providerSdk = new ProviderSDK(provider)
return providerSdk.summaries(messages, assistant)
try {
return await providerSdk.summaries(messages, assistant)
} catch (error: any) {
return null
}
}
export async function checkApi(provider: Provider) {
@@ -92,15 +102,13 @@ export async function checkApi(provider: Provider) {
const providerSdk = new ProviderSDK(provider)
const { valid, error } = await providerSdk.check()
const { valid } = await providerSdk.check()
window.message[valid ? 'success' : 'error']({
key: 'api-check',
style: { marginTop: '3vh' },
duration: valid ? 2 : 8,
content: valid
? i18n.t('message.api.connection.success')
: i18n.t('message.api.connection.failed') + ' : ' + getErrorMessage(error)
content: valid ? i18n.t('message.api.connection.success') : i18n.t('message.api.connection.failed')
})
return valid

View File

@@ -10,5 +10,9 @@ export const EVENT_NAMES = {
ADD_ASSISTANT: 'ADD_ASSISTANT',
EDIT_MESSAGE: 'EDIT_MESSAGE',
REGENERATE_MESSAGE: 'REGENERATE_MESSAGE',
CHAT_COMPLETION_PAUSED: 'CHAT_COMPLETION_PAUSED'
CHAT_COMPLETION_PAUSED: 'CHAT_COMPLETION_PAUSED',
ESTIMATED_TOKEN_COUNT: 'ESTIMATED_TOKEN_COUNT',
SHOW_CHAT_SETTINGS: 'SHOW_CHAT_SETTINGS',
SHOW_TOPIC_SIDEBAR: 'SHOW_TOPIC_SIDEBAR',
SWITCH_TOPIC_SIDEBAR: 'SWITCH_TOPIC_SIDEBAR'
}

View File

@@ -1,7 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { getDefaultAssistant, getDefaultTopic } from '@renderer/services/assistant'
import LocalStorage from '@renderer/services/storage'
import { Assistant, Model, Topic } from '@renderer/types'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import { uniqBy } from 'lodash'
export interface AssistantsState {
@@ -33,6 +33,16 @@ const assistantsSlice = createSlice({
updateAssistant: (state, action: PayloadAction<Assistant>) => {
state.assistants = state.assistants.map((c) => (c.id === action.payload.id ? action.payload : c))
},
updateAssistantSettings: (state, action: PayloadAction<{ assistantId: string; settings: AssistantSettings }>) => {
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
? {
...assistant,
settings: action.payload.settings
}
: assistant
)
},
addTopic: (state, action: PayloadAction<{ assistantId: string; topic: Topic }>) => {
state.assistants = state.assistants.map((assistant) =>
assistant.id === action.payload.assistantId
@@ -111,7 +121,8 @@ export const {
updateTopic,
updateTopics,
removeAllTopics,
setModel
setModel,
updateAssistantSettings
} = assistantsSlice.actions
export default assistantsSlice.reducer

View File

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

View File

@@ -94,6 +94,15 @@ const initialState: LlmState = {
isSystem: true,
enabled: false
},
{
id: 'aihubmix',
name: 'AiHubMix',
apiKey: '',
apiHost: 'https://aihubmix.com',
models: SYSTEM_MODELS.aihubmix.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{
id: 'openrouter',
name: 'OpenRouter',

View File

@@ -5,8 +5,7 @@ import { isEmpty } from 'lodash'
import i18n from '@renderer/i18n'
import { Assistant } from '@renderer/types'
const migrate = createMigrate({
// @ts-ignore store type is unknown
const migrateConfig = {
'2': (state: RootState) => {
return {
...state,
@@ -26,7 +25,6 @@ const migrate = createMigrate({
}
}
},
// @ts-ignore store type is unknown
'3': (state: RootState) => {
return {
...state,
@@ -46,7 +44,6 @@ const migrate = createMigrate({
}
}
},
// @ts-ignore store type is unknown
'4': (state: RootState) => {
return {
...state,
@@ -66,7 +63,6 @@ const migrate = createMigrate({
}
}
},
// @ts-ignore store type is unknown
'5': (state: RootState) => {
return {
...state,
@@ -86,7 +82,6 @@ const migrate = createMigrate({
}
}
},
// @ts-ignore store type is unknown
'6': (state: RootState) => {
return {
...state,
@@ -106,7 +101,6 @@ const migrate = createMigrate({
}
}
},
// @ts-ignore store type is unknown
'7': (state: RootState) => {
return {
...state,
@@ -116,7 +110,6 @@ const migrate = createMigrate({
}
}
},
// @ts-ignore store type is unknown
'8': (state: RootState) => {
const fixAssistantName = (assistant: Assistant) => {
if (isEmpty(assistant.name)) {
@@ -142,7 +135,6 @@ const migrate = createMigrate({
}
}
},
// @ts-ignore store type is unknown
'9': (state: RootState) => {
return {
...state,
@@ -157,7 +149,6 @@ const migrate = createMigrate({
}
}
},
// @ts-ignore store type is unknown
'10': (state: RootState) => {
return {
...state,
@@ -178,7 +169,6 @@ const migrate = createMigrate({
}
}
},
// @ts-ignore store type is unknown
'11': (state: RootState) => {
return {
...state,
@@ -207,7 +197,73 @@ const migrate = createMigrate({
]
}
}
},
'12': (state: RootState) => {
return {
...state,
llm: {
...state.llm,
providers: [
...state.llm.providers,
{
id: 'aihubmix',
name: 'AiHubMix',
apiKey: '',
apiHost: 'https://aihubmix.com',
models: SYSTEM_MODELS.aihubmix.filter((m) => m.enabled),
isSystem: true,
enabled: false
}
]
}
}
},
'13': (state: RootState) => {
return {
...state,
assistants: {
...state.assistants,
defaultAssistant: {
...state.assistants.defaultAssistant,
name: ['Default Assistant', '默认助手'].includes(state.assistants.defaultAssistant.name)
? i18n.t(`assistant.default.name`)
: state.assistants.defaultAssistant.name
}
}
}
},
'14': (state: RootState) => {
return {
...state,
settings: {
...state.settings,
showAssistants: true,
proxyUrl: undefined
}
}
},
'15': (state: RootState) => {
return {
...state,
settings: {
...state.settings,
userName: '',
showMessageDivider: true
}
}
},
'16': (state: RootState) => {
return {
...state,
settings: {
...state.settings,
messageFont: 'system',
showInputEstimatedTokens: false
}
}
}
})
}
const migrate = createMigrate(migrateConfig as any)
export default migrate

View File

@@ -4,14 +4,26 @@ export type SendMessageShortcut = 'Enter' | 'Shift+Enter'
export interface SettingsState {
showRightSidebar: boolean
showAssistants: boolean
sendMessageShortcut: SendMessageShortcut
language: string
proxyUrl?: string
userName: string
showMessageDivider: boolean
messageFont: 'system' | 'serif'
showInputEstimatedTokens: boolean
}
const initialState: SettingsState = {
showRightSidebar: true,
showAssistants: true,
sendMessageShortcut: 'Enter',
language: navigator.language
language: navigator.language,
proxyUrl: undefined,
userName: '',
showMessageDivider: true,
messageFont: 'system',
showInputEstimatedTokens: false
}
const settingsSlice = createSlice({
@@ -21,15 +33,47 @@ const settingsSlice = createSlice({
toggleRightSidebar: (state) => {
state.showRightSidebar = !state.showRightSidebar
},
setShowRightSidebar: (state, action: PayloadAction<boolean>) => {
state.showRightSidebar = action.payload
},
toggleShowAssistants: (state) => {
state.showAssistants = !state.showAssistants
},
setSendMessageShortcut: (state, action: PayloadAction<SendMessageShortcut>) => {
state.sendMessageShortcut = action.payload
},
setLanguage: (state, action: PayloadAction<string>) => {
state.language = action.payload
},
setProxyUrl: (state, action: PayloadAction<string | undefined>) => {
state.proxyUrl = action.payload
},
setUserName: (state, action: PayloadAction<string>) => {
state.userName = action.payload
},
setShowMessageDivider: (state, action: PayloadAction<boolean>) => {
state.showMessageDivider = action.payload
},
setMessageFont: (state, action: PayloadAction<'system' | 'serif'>) => {
state.messageFont = action.payload
},
setShowInputEstimatedTokens: (state, action: PayloadAction<boolean>) => {
state.showInputEstimatedTokens = action.payload
}
}
})
export const { toggleRightSidebar, setSendMessageShortcut, setLanguage } = settingsSlice.actions
export const {
setShowRightSidebar,
toggleRightSidebar,
toggleShowAssistants,
setSendMessageShortcut,
setLanguage,
setProxyUrl,
setUserName,
setShowMessageDivider,
setMessageFont,
setShowInputEstimatedTokens
} = settingsSlice.actions
export default settingsSlice.reducer

View File

@@ -7,6 +7,12 @@ export type Assistant = {
prompt: string
topics: Topic[]
model?: Model
settings?: AssistantSettings
}
export type AssistantSettings = {
contextCount: number
temperature: number
}
export type Message = {

View File

@@ -1,6 +1,9 @@
import { v4 as uuidv4 } from 'uuid'
import imageCompression from 'browser-image-compression'
import { Model } from '@renderer/types'
import { Assistant, AssistantSettings, Message, Model } from '@renderer/types'
import { GPTTokens } from 'gpt-tokens'
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
import { takeRight } from 'lodash'
export const runAsyncFunction = async (fn: () => void) => {
await fn()
@@ -164,3 +167,56 @@ export function getFirstCharacter(str) {
return char
}
}
export const getAssistantSettings = (assistant: Assistant): AssistantSettings => {
const contextCount = assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT
return {
contextCount: contextCount === 20 ? 100000 : contextCount,
temperature: assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE
}
}
export function estimateInputTokenCount(text: string) {
const input = new GPTTokens({
model: 'gpt-4o',
messages: [{ role: 'user', content: text }]
})
return input.usedTokens - 7
}
export function estimateHistoryTokenCount(assistant: Assistant, msgs: Message[]) {
const { contextCount } = getAssistantSettings(assistant)
const all = new GPTTokens({
model: 'gpt-4o',
messages: [
{ role: 'system', content: assistant.prompt },
...takeRight(msgs, contextCount).map((message) => ({ role: message.role, content: message.content }))
]
})
return all.usedTokens - 7
}
/**
* is valid proxy url
* @param url proxy url
* @returns boolean
*/
export const isValidProxyUrl = (url: string) => {
return url.includes('://')
}
export function loadScript(url: string) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = url
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
}

View File

@@ -51,6 +51,7 @@
height: 100px;
margin-bottom: 20px;
border-radius: 10%;
margin-top: -10vh;
}
h1 {
font-size: 48px;
@@ -64,6 +65,7 @@
.download-buttons {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
margin-bottom: 30px;
}
@@ -107,9 +109,17 @@
color: #ffffff;
text-decoration: underline;
}
.loading {
flex-direction: row;
justify-content: center;
height: 200px;
}
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<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>
@@ -117,7 +127,7 @@
<p class="description">Windows/macOS GPT 客户端</p>
<div class="download-buttons">
<a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.8/Cherry-Studio-0.2.8-x64.dmg"
: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
@@ -127,7 +137,7 @@
macOS Intel
</a>
<a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.8/Cherry-Studio-0.2.8-arm64.dmg"
: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
@@ -137,7 +147,7 @@
macOS Apple Silicon
</a>
<a
href="https://github.com/kangfenmao/cherry-studio/releases/download/v0.2.8/Cherry-Studio-0.2.8-setup.exe"
: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
@@ -149,7 +159,10 @@
</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/v0.2.8" target="_blank">v0.2.8</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> |
@@ -172,5 +185,25 @@
}
}
</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>

View File

@@ -961,13 +961,6 @@ __metadata:
languageName: node
linkType: hard
"@fontsource/inter@npm:^5.0.18":
version: 5.0.18
resolution: "@fontsource/inter@npm:5.0.18"
checksum: 10c0/87863160b18a3a2b2cc0b2949b41b4072f422efb672f183a8fa2ae19bd803da18ec24a65344c42c1686c73e1b65bb4a7cd64ce5c0f015872c5f5dcba4e64bf92
languageName: node
linkType: hard
"@hello-pangea/dnd@npm:^16.6.0":
version: 16.6.0
resolution: "@hello-pangea/dnd@npm:16.6.0"
@@ -3413,7 +3406,6 @@ __metadata:
"@electron-toolkit/preload": "npm:^3.0.0"
"@electron-toolkit/tsconfig": "npm:^1.0.1"
"@electron-toolkit/utils": "npm:^3.0.0"
"@fontsource/inter": "npm:^5.0.18"
"@hello-pangea/dnd": "npm:^16.6.0"
"@kangfenmao/keyv-storage": "npm:^0.1.0"
"@reduxjs/toolkit": "npm:^2.2.5"
@@ -3440,6 +3432,7 @@ __metadata:
eslint-plugin-react: "npm:^7.34.3"
eslint-plugin-react-hooks: "npm:^4.6.2"
eslint-plugin-unused-imports: "npm:^4.0.0"
gpt-tokens: "npm:^1.3.6"
i18next: "npm:^23.11.5"
localforage: "npm:^1.10.0"
lodash: "npm:^4.17.21"
@@ -3797,6 +3790,13 @@ __metadata:
languageName: node
linkType: hard
"decimal.js@npm:^10.4.3":
version: 10.4.3
resolution: "decimal.js@npm:10.4.3"
checksum: 10c0/6d60206689ff0911f0ce968d40f163304a6c1bc739927758e6efc7921cfa630130388966f16bf6ef6b838cb33679fbe8e7a78a2f3c478afce841fd55ac8fb8ee
languageName: node
linkType: hard
"decode-named-character-reference@npm:^1.0.0":
version: 1.0.2
resolution: "decode-named-character-reference@npm:1.0.2"
@@ -5241,6 +5241,17 @@ __metadata:
languageName: node
linkType: hard
"gpt-tokens@npm:^1.3.6":
version: 1.3.6
resolution: "gpt-tokens@npm:1.3.6"
dependencies:
decimal.js: "npm:^10.4.3"
js-tiktoken: "npm:^1.0.10"
openai-chat-tokens: "npm:^0.2.8"
checksum: 10c0/0efc1da655a16a306df4f17646832693d7cbec569fe44d4fcc9d4a605f8614f1eb974e04b24a4e8c71095fe0fab6de7251a34c6e2d6805a5e1b5811eea37437b
languageName: node
linkType: hard
"graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.6":
version: 4.2.11
resolution: "graceful-fs@npm:4.2.11"
@@ -6067,6 +6078,15 @@ __metadata:
languageName: node
linkType: hard
"js-tiktoken@npm:^1.0.10, js-tiktoken@npm:^1.0.7":
version: 1.0.12
resolution: "js-tiktoken@npm:1.0.12"
dependencies:
base64-js: "npm:^1.5.1"
checksum: 10c0/7afb4826e21342386a1884754fbc1c1828f948c4dd0ab093bf778d1323e65343bd5343d15f7cda46af396f1fe4a0297739936149b7c40a0601eefe3fcaef8727
languageName: node
linkType: hard
"js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0":
version: 4.0.0
resolution: "js-tokens@npm:4.0.0"
@@ -7194,6 +7214,15 @@ __metadata:
languageName: node
linkType: hard
"openai-chat-tokens@npm:^0.2.8":
version: 0.2.8
resolution: "openai-chat-tokens@npm:0.2.8"
dependencies:
js-tiktoken: "npm:^1.0.7"
checksum: 10c0/b415fda706b408f29b4584998990f29ad7f80f2ac1e84179a0976742ba8a80859fedeae5745a9bfe73443d95960b77328610074952ad198a18bc0e5c0ceb5b7b
languageName: node
linkType: hard
"openai@npm:^4.52.1":
version: 4.52.1
resolution: "openai@npm:4.52.1"