Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0096783f26 | ||
|
|
4fc53d7c19 | ||
|
|
34d99b711c | ||
|
|
5dd74a1018 | ||
|
|
e028d0600f | ||
|
|
64ee3f2108 | ||
|
|
30a082b979 | ||
|
|
5a0927393d | ||
|
|
16c68dcdcb | ||
|
|
b6500977b0 | ||
|
|
78cf33e8bc | ||
|
|
2f62f04adf | ||
|
|
84915b1ede | ||
|
|
248c7ea20e | ||
|
|
1031d40ddb | ||
|
|
3d44fc2208 | ||
|
|
22e3c0e270 | ||
|
|
5d81874166 | ||
|
|
f7ef895ce6 | ||
|
|
beb40f5baf | ||
|
|
07613e65f5 | ||
|
|
6185068353 | ||
|
|
61934cd65c | ||
|
|
41f65b66ba | ||
|
|
5edb53ef7d | ||
|
|
167988927b | ||
|
|
a39beb3841 | ||
|
|
8719d5c330 | ||
|
|
a7427d6cb6 | ||
|
|
8759a50727 | ||
|
|
7ffa42caa0 | ||
|
|
b0a3d705ff | ||
|
|
de41199f7e | ||
|
|
cbd9f60cfc | ||
|
|
8a0e2890dd | ||
|
|
a8f3e2be6b | ||
|
|
297539bab7 | ||
|
|
911c2d0202 | ||
|
|
2969a05f10 | ||
|
|
5d90489a04 | ||
|
|
18fa1c92a4 | ||
|
|
937e62bf9d | ||
|
|
6291a463d8 |
@@ -1,5 +1,5 @@
|
||||
module.exports = {
|
||||
plugins: ['unused-imports'],
|
||||
plugins: ['unused-imports', 'simple-import-sort'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
@@ -14,12 +14,7 @@ module.exports = {
|
||||
'unused-imports/no-unused-imports': 'error',
|
||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'sort-imports': [
|
||||
'error',
|
||||
{
|
||||
ignoreCase: true,
|
||||
ignoreDeclarationSort: true
|
||||
}
|
||||
]
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error'
|
||||
}
|
||||
}
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest]
|
||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Check out Git repository
|
||||
|
||||
27
README.md
27
README.md
@@ -1,18 +1,18 @@
|
||||
# Cherry Studio
|
||||
# 🍒 Cherry Studio
|
||||
|
||||
🍒 Cherry Studio is a desktop client that supports multiple Large Language Model (LLM) providers, available on Windows, Mac and Linux.
|
||||
Cherry Studio is a desktop client that supports for multiple LLM providers, available on Windows, Mac and Linux.
|
||||
|
||||
# Screenshot
|
||||
# 🌠 Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
# Feature
|
||||
# 🌟 Features
|
||||
|
||||
1. Supports multiple large language model service providers.
|
||||
1. Support for Multiple LLM Providers.
|
||||
2. Allows creation of multiple Assistants.
|
||||
3. Enables creation of multiple topics.
|
||||
4. Allows using multiple models to answer questions in the same conversation.
|
||||
@@ -20,7 +20,8 @@
|
||||
6. Code highlighting.
|
||||
7. Mermaid chart
|
||||
|
||||
# Develop
|
||||
# 🖥️ Develop
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)
|
||||
@@ -51,3 +52,11 @@ $ yarn build:mac
|
||||
# For Linux
|
||||
$ yarn build:linux
|
||||
```
|
||||
|
||||
# ⭐️ Star History
|
||||
|
||||
[](https://star-history.com/#kangfenmao/cherry-studio&Timeline)
|
||||
|
||||
# 📃 License
|
||||
|
||||
[LICENSE](./LICENSE)
|
||||
|
||||
@@ -40,8 +40,8 @@ dmg:
|
||||
linux:
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
# - snap
|
||||
# - deb
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
appImage:
|
||||
@@ -56,5 +56,4 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
新增发送按钮
|
||||
输入区域展开可以全屏显示
|
||||
错误修复,优化体验
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cherry-studio",
|
||||
"version": "0.4.6",
|
||||
"version": "0.5.3",
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "kangfenmao@qq.com",
|
||||
@@ -29,7 +29,8 @@
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"electron-window-state": "^5.0.3"
|
||||
"electron-window-state": "^5.0.3",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.24.3",
|
||||
@@ -54,6 +55,7 @@
|
||||
"electron-devtools-installer": "^3.2.0",
|
||||
"electron-vite": "^2.0.0",
|
||||
"emittery": "^1.0.3",
|
||||
"emoji-picker-element": "^1.22.1",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.34.3",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 197 KiB |
@@ -1,6 +1,6 @@
|
||||
import { dialog, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
||||
import { writeFile } from 'fs'
|
||||
import logger from 'electron-log'
|
||||
import { writeFile } from 'fs'
|
||||
|
||||
export async function saveFile(_: Electron.IpcMainInvokeEvent, fileName: string, content: string): Promise<void> {
|
||||
try {
|
||||
|
||||
@@ -4,7 +4,8 @@ import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, shell } from 'ele
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
|
||||
import icon from '../../build/icon.png?asset'
|
||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { saveFile } from './event'
|
||||
import AppUpdater from './updater'
|
||||
@@ -25,7 +26,7 @@ function createWindow() {
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 500,
|
||||
minHeight: 600,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: process.platform === 'darwin',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
|
||||
import logger from 'electron-log'
|
||||
import { BrowserWindow, dialog } from 'electron'
|
||||
import logger from 'electron-log'
|
||||
import { AppUpdater as _AppUpdater, autoUpdater, UpdateInfo } from 'electron-updater'
|
||||
|
||||
export default class AppUpdater {
|
||||
autoUpdater: _AppUpdater = autoUpdater
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
|
||||
@@ -2,9 +2,10 @@ import store, { persistor } from '@renderer/store'
|
||||
import { Provider } from 'react-redux'
|
||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import Sidebar from './components/app/Sidebar'
|
||||
import TopViewContainer from './components/TopView'
|
||||
import AppsPage from './pages/apps/AppsPage'
|
||||
import AgentsPage from './pages/agents/AgentsPage'
|
||||
import HomePage from './pages/home/HomePage'
|
||||
import SettingsPage from './pages/settings/SettingsPage'
|
||||
import TranslatePage from './pages/translate/TranslatePage'
|
||||
@@ -22,7 +23,7 @@ function App(): JSX.Element {
|
||||
<Sidebar />
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="/apps" element={<AppsPage />} />
|
||||
<Route path="/apps" element={<AgentsPage />} />
|
||||
<Route path="/translate" element={<TranslatePage />} />
|
||||
<Route path="/settings/*" element={<SettingsPage />} />
|
||||
</Routes>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,47 +0,0 @@
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-Thin.ttf) format('truetype');
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-ExtraLight.ttf) format('truetype');
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-Light.ttf) format('truetype');
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-Regular.ttf) format('truetype');
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-Medium.ttf) format('truetype');
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-SemiBold.ttf) format('truetype');
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-Bold.ttf) format('truetype');
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Poppins';
|
||||
src: url(Poppins-ExtraBold.ttf) format('truetype');
|
||||
font-weight: 800;
|
||||
}
|
||||
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-Bold.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-Bold.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-BoldItalic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-Italic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-Italic.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-Light.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-Light.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-LightItalic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-LightItalic.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-Medium.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-Medium.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-MediumItalic.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-MediumItalic.ttf
Normal file
Binary file not shown.
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-Regular.ttf
Normal file
BIN
src/renderer/src/assets/fonts/Ubuntu/Ubuntu-Regular.ttf
Normal file
Binary file not shown.
55
src/renderer/src/assets/fonts/Ubuntu/Ubuntu.css
Normal file
55
src/renderer/src/assets/fonts/Ubuntu/Ubuntu.css
Normal file
@@ -0,0 +1,55 @@
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-Regular.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-Italic.ttf') format('truetype');
|
||||
font-weight: normal;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-Bold.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-BoldItalic.ttf') format('truetype');
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-Light.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-LightItalic.ttf') format('truetype');
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-Medium.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
src: url('Ubuntu-MediumItalic.ttf') format('truetype');
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4563475 */
|
||||
src: url('iconfont.woff2?t=1722242729348') format('woff2'),
|
||||
url('iconfont.woff?t=1722242729348') format('woff'),
|
||||
url('iconfont.ttf?t=1722242729348') format('truetype');
|
||||
src: url('iconfont.woff2?t=1723186111414') format('woff2'),
|
||||
url('iconfont.woff?t=1723186111414') format('woff'),
|
||||
url('iconfont.ttf?t=1723186111414') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@@ -13,6 +13,14 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-copy:before {
|
||||
content: "\e6ae";
|
||||
}
|
||||
|
||||
.icon-ic_send:before {
|
||||
content: "\e795";
|
||||
}
|
||||
|
||||
.icon-dark1:before {
|
||||
content: "\e72f";
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,7 +1,7 @@
|
||||
@import './markdown.scss';
|
||||
@import './scrollbar.scss';
|
||||
@import '../fonts/icon-fonts/iconfont.css';
|
||||
@import '../fonts/Poppins/Poppins.css';
|
||||
@import '../fonts/Ubuntu/Ubuntu.css';
|
||||
|
||||
:root {
|
||||
--color-white: #ffffff;
|
||||
@@ -33,12 +33,14 @@
|
||||
--color-icon-white: #ffffff;
|
||||
--color-border: #ffffff20;
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #323232;
|
||||
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
|
||||
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3);
|
||||
|
||||
--navbar-background: rgba(0, 0, 0, 0.8);
|
||||
--sidebar-background: rgba(0, 0, 0, 0.8);
|
||||
--input-bar-background: rgba(255, 255, 255, 0.02);
|
||||
|
||||
--navbar-height: 42px;
|
||||
--sidebar-width: 55px;
|
||||
@@ -46,7 +48,7 @@
|
||||
--topic-list-width: 260px;
|
||||
--settings-width: var(--assistants-width);
|
||||
--status-bar-height: 40px;
|
||||
--input-bar-height: 115px;
|
||||
--input-bar-height: 85px;
|
||||
}
|
||||
|
||||
body[theme-mode='light'] {
|
||||
@@ -79,12 +81,14 @@ body[theme-mode='light'] {
|
||||
--color-icon-white: #000000;
|
||||
--color-border: #00000028;
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #e3e3e3;
|
||||
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
|
||||
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3);
|
||||
|
||||
--navbar-background: rgba(255, 255, 255, 0.8);
|
||||
--sidebar-background: rgba(255, 255, 255, 0.8);
|
||||
--input-bar-background: rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
*,
|
||||
@@ -106,18 +110,8 @@ body {
|
||||
line-height: 1.6;
|
||||
overflow: hidden;
|
||||
background: transparent !important;
|
||||
font-family:
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Microsoft YaHei',
|
||||
'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, 'Open Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@@ -163,3 +157,7 @@ body,
|
||||
position: relative;
|
||||
animation: flash 0.5s ease-out infinite alternate;
|
||||
}
|
||||
|
||||
.ant-segmented-group {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
@@ -3,14 +3,7 @@
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
user-select: text;
|
||||
|
||||
p:last-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
p:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
word-break: break-word;
|
||||
|
||||
h1:first-child,
|
||||
h2:first-child,
|
||||
@@ -29,14 +22,20 @@
|
||||
h6 {
|
||||
margin: 1em 0 1em 0;
|
||||
font-weight: 800;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@@ -57,6 +56,13 @@
|
||||
|
||||
p {
|
||||
margin: 1em 0;
|
||||
&:last-child {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
ul,
|
||||
@@ -67,13 +73,21 @@
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5em;
|
||||
&::marker {
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
}
|
||||
|
||||
li > ul,
|
||||
li > ol {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #555;
|
||||
border-top: 0.5px solid var(--color-border);
|
||||
margin: 20px 0;
|
||||
background-color: #555;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
|
||||
span {
|
||||
@@ -81,7 +95,8 @@
|
||||
}
|
||||
|
||||
code {
|
||||
white-space: pre-wrap;
|
||||
white-space: pre-wrap !important;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
p code {
|
||||
@@ -90,7 +105,141 @@
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
pre pre {
|
||||
margin: 0 !important;
|
||||
pre {
|
||||
white-space: pre-wrap !important;
|
||||
padding: 1em 0;
|
||||
border-radius: 5px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Fira Code', 'Courier New', Courier, monospace;
|
||||
pre {
|
||||
margin: 0 !important;
|
||||
}
|
||||
code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 1em 0;
|
||||
padding-left: 1em;
|
||||
color: var(--color-text-light);
|
||||
border-left: 4px solid var(--color-border);
|
||||
font-family: Georgia, 'Times New Roman', Times, serif;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
margin: 1em 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
border: 0.5px solid var(--color-border);
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--color-background-mute);
|
||||
font-weight: bold;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a,
|
||||
.link {
|
||||
color: var(--color-link);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
del {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
sup,
|
||||
sub {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
.footnote-ref {
|
||||
font-size: 0.8em;
|
||||
vertical-align: super;
|
||||
line-height: 0;
|
||||
margin: 0 2px;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.footnotes {
|
||||
margin-top: 1em;
|
||||
padding-top: 1em;
|
||||
border-top: 1px solid var(--color-border);
|
||||
|
||||
ol {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--color-text-light);
|
||||
|
||||
p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footnote-backref {
|
||||
font-size: 0.8em;
|
||||
vertical-align: super;
|
||||
line-height: 0;
|
||||
margin-left: 5px;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
emoji-picker {
|
||||
--border-size: 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* 全局初始化滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
|
||||
24
src/renderer/src/components/Avatar/ModelAvatar.tsx
Normal file
24
src/renderer/src/components/Avatar/ModelAvatar.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Avatar, AvatarProps } from 'antd'
|
||||
import { first } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
|
||||
interface Props {
|
||||
model: Model
|
||||
size: number
|
||||
props?: AvatarProps
|
||||
}
|
||||
|
||||
const ModelAvatar: FC<Props> = ({ model, size, props }) => {
|
||||
return (
|
||||
<Avatar
|
||||
src={getModelLogo(model?.id || '')}
|
||||
style={{ width: size, height: size, display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||||
{...props}>
|
||||
{first(model?.name)}
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelAvatar
|
||||
49
src/renderer/src/components/DragableList/index.tsx
Normal file
49
src/renderer/src/components/DragableList/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import { FC } from 'react'
|
||||
|
||||
interface Props<T> {
|
||||
list: T[]
|
||||
children: (item: T, index: number) => React.ReactNode
|
||||
onUpdate: (list: T[]) => void
|
||||
onDragStart?: () => void
|
||||
onDragEnd?: () => void
|
||||
}
|
||||
|
||||
const DragableList: FC<Props<any>> = ({ children, list, onDragStart, onUpdate, onDragEnd }) => {
|
||||
const _onDragEnd = (result: DropResult) => {
|
||||
onDragEnd?.()
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
const reorderAgents = droppableReorder(list, sourceIndex, destIndex)
|
||||
onUpdate(reorderAgents)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{list.map((item, index) => (
|
||||
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{ ...provided.draggableProps.style, marginBottom: 8 }}>
|
||||
{children(item, index)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)
|
||||
}
|
||||
|
||||
export default DragableList
|
||||
25
src/renderer/src/components/EmojiPicker/index.tsx
Normal file
25
src/renderer/src/components/EmojiPicker/index.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { FC, useEffect, useRef } from 'react'
|
||||
|
||||
interface Props {
|
||||
onEmojiClick: (emoji: string) => void
|
||||
}
|
||||
|
||||
const EmojiPicker: FC<Props> = ({ onEmojiClick }) => {
|
||||
const { theme } = useTheme()
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.addEventListener('emoji-click', (event: any) => {
|
||||
event.stopPropagation()
|
||||
onEmojiClick(event.detail.emoji.unicode)
|
||||
})
|
||||
}
|
||||
}, [onEmojiClick])
|
||||
|
||||
// @ts-ignore next-line
|
||||
return <emoji-picker ref={ref} class={theme === 'dark' ? 'dark' : 'light'} style={{ border: 'none' }} />
|
||||
}
|
||||
|
||||
export default EmojiPicker
|
||||
@@ -150,11 +150,12 @@ export const BaseTypography = styled(Box)<{
|
||||
`
|
||||
|
||||
export const TypographyNormal = styled(BaseTypography)`
|
||||
font-family: 'Poppins';
|
||||
font-family: 'Ubuntu';
|
||||
`
|
||||
|
||||
export const TypographyBold = styled(BaseTypography)`
|
||||
font-family: 'Poppins Bold';
|
||||
font-family: 'Ubuntu';
|
||||
font-weight: bold;
|
||||
`
|
||||
|
||||
export const Container = styled.main<ContainerProps>`
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Input, Modal } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { TopView } from '../TopView'
|
||||
import { Box } from '../Layout'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Input, Modal } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Box } from '../Layout'
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
interface AssistantSettingPopupShowParams {
|
||||
assistant: Assistant
|
||||
}
|
||||
@@ -56,18 +57,19 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
|
||||
export default class AssistantSettingPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(this.topviewId)
|
||||
TopView.hide('AssistantSettingPopup')
|
||||
}
|
||||
static show(props: AssistantSettingPopupShowParams) {
|
||||
return new Promise<Assistant>((resolve) => {
|
||||
this.topviewId = TopView.show(
|
||||
TopView.show(
|
||||
<AssistantSettingPopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
'AssistantSettingPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Input, InputProps, Modal } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
import { Box } from '../Layout'
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
interface PromptPopupShowParams {
|
||||
title: string
|
||||
@@ -57,18 +58,19 @@ const PromptPopupContainer: React.FC<Props> = ({
|
||||
export default class PromptPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(this.topviewId)
|
||||
TopView.hide('PromptPopup')
|
||||
}
|
||||
static show(props: PromptPopupShowParams) {
|
||||
return new Promise<string>((resolve) => {
|
||||
this.topviewId = TopView.show(
|
||||
TopView.show(
|
||||
<PromptPopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
'PromptPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Modal } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
import { Box } from '../Layout'
|
||||
import { TopView } from '../TopView'
|
||||
|
||||
interface ShowParams {
|
||||
title: string
|
||||
@@ -36,18 +37,19 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||
export default class TemplatePopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(this.topviewId)
|
||||
TopView.hide('TemplatePopup')
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
this.topviewId = TopView.show(
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
'TemplatePopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,87 +1,94 @@
|
||||
import { useAppInit } from '@renderer/hooks/useAppInit'
|
||||
import { message, Modal } from 'antd'
|
||||
import { findIndex, pullAt } from 'lodash'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { PropsWithChildren, useCallback, useEffect, useRef, useState } from 'react'
|
||||
|
||||
import { Box } from '../Layout'
|
||||
|
||||
let id = 0
|
||||
let onPop = () => {}
|
||||
|
||||
let onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => {
|
||||
let onShow = ({ element, id }: { element: React.FC | React.ReactNode; id: string }) => {
|
||||
element
|
||||
key
|
||||
id
|
||||
}
|
||||
|
||||
let onHide = ({ key }: { key: number }) => {
|
||||
key
|
||||
let onHide = (id: string) => {
|
||||
id
|
||||
}
|
||||
let onHideAll = () => {}
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
type ElementItem = {
|
||||
key: number
|
||||
id: string
|
||||
element: React.FC | React.ReactNode
|
||||
}
|
||||
|
||||
const TopViewContainer: React.FC<Props> = ({ children }) => {
|
||||
const [elements, setElements] = useState<ElementItem[]>([])
|
||||
const elementsRef = useRef<ElementItem[]>([])
|
||||
elementsRef.current = elements
|
||||
|
||||
const [messageApi, messageContextHolder] = message.useMessage()
|
||||
const [modal, modalContextHolder] = Modal.useModal()
|
||||
|
||||
useAppInit()
|
||||
|
||||
onPop = () => {
|
||||
const views = [...elements]
|
||||
views.pop()
|
||||
setElements(views)
|
||||
}
|
||||
|
||||
onShow = ({ element, key }: { element: React.FC | React.ReactNode; key: number }) => {
|
||||
setElements(elements.concat([{ element, key }]))
|
||||
}
|
||||
|
||||
onHide = ({ key }: { key: number }) => {
|
||||
const views = [...elements]
|
||||
pullAt(views, findIndex(views, { key }))
|
||||
setElements(views)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
window.message = messageApi
|
||||
window.modal = modal
|
||||
}, [messageApi, modal])
|
||||
|
||||
onPop = () => {
|
||||
const views = [...elementsRef.current]
|
||||
views.pop()
|
||||
elementsRef.current = views
|
||||
setElements(elementsRef.current)
|
||||
}
|
||||
|
||||
onShow = ({ element, id }: ElementItem) => {
|
||||
if (!elementsRef.current.find((el) => el.id === id)) {
|
||||
elementsRef.current = elementsRef.current.concat([{ element, id }])
|
||||
setElements(elementsRef.current)
|
||||
}
|
||||
}
|
||||
|
||||
onHide = (id: string) => {
|
||||
elementsRef.current = elementsRef.current.filter((el) => el.id !== id)
|
||||
setElements(elementsRef.current)
|
||||
}
|
||||
|
||||
onHideAll = () => {
|
||||
setElements([])
|
||||
elementsRef.current = []
|
||||
}
|
||||
|
||||
const FullScreenContainer: React.FC<PropsWithChildren> = useCallback(({ children }) => {
|
||||
return (
|
||||
<Box flex={1} position="absolute" w="100%" h="100%">
|
||||
<Box position="absolute" w="100%" h="100%" onClick={onPop} />
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{messageContextHolder}
|
||||
{modalContextHolder}
|
||||
{elements.length > 0 && (
|
||||
<div style={{ display: 'flex', flex: 1, position: 'absolute', width: '100%', height: '100%' }}>
|
||||
<div style={{ position: 'absolute', width: '100%', height: '100%' }} onClick={onPop} />
|
||||
{elements.map(({ element: Element, key }) =>
|
||||
typeof Element === 'function' ? (
|
||||
<Element key={`TOPVIEW_${key}`} />
|
||||
) : (
|
||||
<div key={`TOPVIEW_${key}`}>{Element}</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{elements.map(({ element: Element, id }) => (
|
||||
<FullScreenContainer key={`TOPVIEW_${id}`}>
|
||||
{typeof Element === 'function' ? <Element /> : Element}
|
||||
</FullScreenContainer>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export const TopView = {
|
||||
show: (element: React.FC | React.ReactNode) => {
|
||||
id = id + 1
|
||||
onShow({ element, key: id })
|
||||
return id
|
||||
},
|
||||
hide: (key: number) => {
|
||||
onHide({ key })
|
||||
},
|
||||
show: (element: React.FC | React.ReactNode, id: string) => onShow({ element, id }),
|
||||
hide: (id: string) => onHide(id),
|
||||
clear: () => onHideAll(),
|
||||
pop: onPop
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { FC } from 'react'
|
||||
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 { isWindows } from '@renderer/config/constant'
|
||||
import { TranslationOutlined } from '@ant-design/icons'
|
||||
import Logo from '@renderer/assets/images/logo.png'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { FC } from 'react'
|
||||
import { Link, useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const Sidebar: FC = () => {
|
||||
const { pathname } = useLocation()
|
||||
@@ -13,7 +12,7 @@ const Sidebar: FC = () => {
|
||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||
|
||||
return (
|
||||
<Container style={isWindows ? { paddingTop: 0 } : {}}>
|
||||
<Container>
|
||||
<StyledLink to="/">
|
||||
<AvatarImg src={avatar || Logo} draggable={false} />
|
||||
</StyledLink>
|
||||
|
||||
@@ -1,239 +1,239 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "🎯 产品经理 - Product Manager",
|
||||
"id": "1",
|
||||
"name": "产品经理 - Product Manager",
|
||||
"emoji": "🎯",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名经验丰富的产品经理,你具有深厚的技术背景,并且对市场和用户需求有敏锐的洞察力。你擅长解决复杂的问题,制定有效的产品策略,并优秀地平衡各种资源以实现产品目标。你具有卓越的项目管理能力和出色的沟通技巧,能够有效地协调团队内部和外部的资源。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "🎯 策略产品经理 - Strategy Product Manager",
|
||||
"emoji": "🎯",
|
||||
"id": "2",
|
||||
"name": "策略产品经理 - Strategy Product Manager",
|
||||
"emoji": "🎯 ",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名策略产品经理,你擅长进行市场研究和竞品分析,以制定产品策略。你能把握行业趋势,了解用户需求,并在此基础上优化产品功能和用户体验。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "👥 社群运营 - Community Operations",
|
||||
"id": "3",
|
||||
"name": "社群运营 - Community Operations",
|
||||
"emoji": "👥",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名社群运营专家,你擅长激发社群活力,增强用户的参与度和忠诚度。你了解如何管理和引导社群文化,以及如何解决社群内的问题和冲突。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "✍️ 内容运营 - Content Operations",
|
||||
"id": "4",
|
||||
"name": "内容运营 - Content Operations",
|
||||
"emoji": "✍️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名专业的内容运营人员,你精通内容创作、编辑、发布和优化。你对读者需求有敏锐的感知,擅长通过高质量的内容吸引和保留用户。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "🛍️ 商家运营 - Merchant Operations",
|
||||
"id": "5",
|
||||
"name": "商家运营 - Merchant Operations",
|
||||
"emoji": "🛍️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名经验丰富的商家运营专家,你擅长管理商家关系,优化商家业务流程,提高商家满意度。你对电商行业有深入的了解,并有优秀的商业洞察力。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "🚀 产品运营 - Product Operations",
|
||||
"id": "6",
|
||||
"name": "产品运营 - Product Operations",
|
||||
"emoji": "🚀",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名经验丰富的产品运营专家,你擅长分析市场和用户需求,并对产品生命周期各阶段的运营策略有深刻的理解。你有出色的团队协作能力和沟通技巧,能在不同部门间进行有效的协调。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"name": "💼 销售运营 - Sales Operations",
|
||||
"emoji": "🎓",
|
||||
"id": "7",
|
||||
"name": "销售运营 - Sales Operations",
|
||||
"emoji": "💼",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名销售运营经理,你懂得如何优化销售流程,管理销售数据,提升销售效率。你能制定销售预测和目标,管理销售预算,并提供销售支持。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"name": "👨💻 用户运营 - User Operations",
|
||||
"id": "8",
|
||||
"name": "用户运营 - User Operations",
|
||||
"emoji": "👨💻",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名用户运营专家,你了解用户行为和需求,能够制定并执行针对性的用户运营策略。你有出色的用户服务能力,能有效处理用户反馈和投诉。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"name": "📢 市场营销 - Marketing",
|
||||
"id": "9",
|
||||
"name": "市场营销 - Marketing",
|
||||
"emoji": "📢",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名专业的市场营销专家,你对营销策略和品牌推广有深入的理解。你熟知如何有效利用不同的渠道和工具来达成营销目标,并对消费者心理有深入的理解。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"name": "📈 商业数据分析 - Business Data Analysis",
|
||||
"id": "10",
|
||||
"name": "商业数据分析 - Business Data Analysis",
|
||||
"emoji": "📈",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名商业数据分析师,你精通数据分析方法和工具,能够从大量数据中提取出有价值的商业洞察。你对业务运营有深入的理解,并能提供数据驱动的优化建议。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"name": "🗂️ 项目管理 - Project Management",
|
||||
"id": "11",
|
||||
"name": "项目管理 - Project Management",
|
||||
"emoji": "🗂️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名资深的项目经理,你精通项目管理的各个方面,包括规划、组织、执行和控制。你擅长处理项目风险,解决问题,并有效地协调团队成员以实现项目目标。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
"name": "🔎 SEO专家 - SEO Expert",
|
||||
"id": "12",
|
||||
"name": "SEO专家 - SEO Expert",
|
||||
"emoji": "🔎",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名知识丰富的SEO专家,你了解搜索引擎的工作原理,熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名知识丰富的SEO专家,你了解搜索引擎的工作原理,熟知如何优化网页以提高其在搜索引擎中的排名。你对关键词研究、内容优化、链接建设等SEO策略有深入的了解。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"name": "💻 网站运营数据分析 - Website Operations Data Analysis",
|
||||
"id": "13",
|
||||
"name": "网站运营数据分析 - Website Operations Data Analysis",
|
||||
"emoji": "💻",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名网站运营数据分析师,你擅长收集和分析网站数据,以了解用户行为和网站性能。你可以提供关于网站设计、内容和营销策略的数据支持。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"name": "📊 数据分析师 - Data Analyst",
|
||||
"id": "14",
|
||||
"name": "数据分析师 - Data Analyst",
|
||||
"emoji": "📊",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名数据分析师,你精通各种统计分析方法,懂得如何清洗、处理和解析数据以获得有价值的洞察。你擅长利用数据驱动的方式来解决问题和提升决策效率。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"name": "🖥️ 前端工程师 - Frontend Engineer",
|
||||
"id": "15",
|
||||
"name": "前端工程师 - Frontend Engineer",
|
||||
"emoji": "🖥️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名专业的前端工程师,你对HTML、CSS、JavaScript等前端技术有深入的了解,能够制作和优化用户界面。你能够解决浏览器兼容性问题,提升网页性能,并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名专业的前端工程师,你对HTML、CSS、JavaScript等前端技术有深入的了解,能够制作和优化用户界面。你能够解决浏览器兼容性问题,提升网页性能,并实现优秀的用户体验。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"name": "🛠️ 运维工程师 - Operations Engineer",
|
||||
"id": "16",
|
||||
"name": "运维工程师 - Operations Engineer",
|
||||
"emoji": "🛠️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名运维工程师,你负责保障系统和服务的正常运行。你熟悉各种监控工具,能够高效地处理故障和进行系统优化。你还懂得如何进行数据备份和恢复,以保证数据安全。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"name": "💻 开发工程师 - Software Engineer",
|
||||
"id": "17",
|
||||
"name": "开发工程师 - Software Engineer",
|
||||
"emoji": "💻",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名资深的软件工程师,你熟悉多种编程语言和开发框架,对软件开发的生命周期有深入的理解。你擅长解决技术问题,并具有优秀的逻辑思维能力。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "🧪 测试工程师 - Test Engineer",
|
||||
"id": "18",
|
||||
"name": "测试工程师 - Test Engineer",
|
||||
"emoji": "🧪",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名专业的测试工程师,你对软件测试方法论和测试工具有深入的了解。你的主要任务是发现和记录软件的缺陷,并确保软件的质量。你在寻找和解决问题上有出色的技能。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "👥 HR人力资源管理 - Human Resources Management",
|
||||
"id": "19",
|
||||
"name": "HR人力资源管理 - Human Resources Management",
|
||||
"emoji": "👥",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名人力资源管理专家,你了解如何招聘、培训、评估和激励员工。你精通劳动法规,擅长处理员工关系,并且在组织发展和变革管理方面有深入的见解。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "📋 行政 - Administration",
|
||||
"id": "20",
|
||||
"name": "行政 - Administration",
|
||||
"emoji": "📋",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名行政专员,你擅长组织和管理公司的日常运营事务,包括文件管理、会议安排、办公设施管理等。你有良好的人际沟通和组织能力,能在多任务环境中有效工作。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"name": "💰 财务顾问 - Financial Advisor",
|
||||
"id": "21",
|
||||
"name": "财务顾问 - Financial Advisor",
|
||||
"emoji": "💰",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名财务顾问,你对金融市场、投资策略和财务规划有深厚的理解。你能提供财务咨询服务,帮助客户实现其财务目标。你擅长理解和解决复杂的财务问题。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "🩺 医生 - Doctor",
|
||||
"id": "22",
|
||||
"name": "医生 - Doctor",
|
||||
"emoji": "🩺",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名医生,具备丰富的医学知识和临床经验。你擅长诊断和治疗各种疾病,能为病人提供专业的医疗建议。你有良好的沟通技巧,能与病人和他们的家人建立信任关系。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "✒️ 编辑 - Editor",
|
||||
"id": "23",
|
||||
"name": "编辑 - Editor",
|
||||
"emoji": "✒️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名编辑,你对文字有敏锐的感觉,擅长审校和修订稿件以确保其质量。你有出色的语言和沟通技巧,能与作者有效地合作以改善他们的作品。你对出版流程有深入的了解。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "🧠 哲学家 - Philosopher",
|
||||
"id": "24",
|
||||
"name": "哲学家 - Philosopher",
|
||||
"emoji": "🧠",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名哲学家,你对世界的本质和人类存在的意义有深入的思考。你熟悉多种哲学流派,并能从哲学的角度分析和解决问题。你具有深刻的思维和出色的逻辑分析能力。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "🛒 采购 - Procurement",
|
||||
"id": "25",
|
||||
"name": "采购 - Procurement",
|
||||
"emoji": "🛒",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n",
|
||||
"description": "你现在是一名采购经理,你熟悉供应链管理,擅长进行供应商评估和价格谈判。你负责制定和执行采购策略,以保证货物的质量和供应的稳定。请在这个角色下为我解答以下问题。\n"
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "⚖️ 法务 - Legal Affairs",
|
||||
"id": "26",
|
||||
"name": "法务 - Legal Affairs",
|
||||
"emoji": "⚖️",
|
||||
"group": "职业",
|
||||
"prompt": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。",
|
||||
"description": "你现在是一名法务专家,你了解公司法、合同法等相关法律,能为企业提供法律咨询和风险评估。你还擅长处理法律争端,并能起草和审核合同。请在这个角色下为我解答以下问题。"
|
||||
},
|
||||
{
|
||||
"id": 27,
|
||||
"name": "🇨🇳 翻译成中文 - Chinese",
|
||||
"id": "27",
|
||||
"name": "翻译成中文 - Chinese",
|
||||
"emoji": "🇨🇳",
|
||||
"group": "语言",
|
||||
"prompt": "你是一个好用的翻译助手。请将我的英文翻译成中文,将所有非中文的翻译成中文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合中文的语言习惯。",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 28,
|
||||
"name": "🌐 翻译成英文 - English",
|
||||
"id": "28",
|
||||
"name": "翻译成英文 - English",
|
||||
"emoji": "🌐",
|
||||
"group": "语言",
|
||||
"prompt": "你是一个好用的翻译助手。请将我的中文翻译成英文,将所有非中文的翻译成英文。我发给你所有的话都是需要翻译的内容,你只需要回答翻译结果。翻译结果请符合英文的语言习惯。",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 29,
|
||||
"name": "📕 英语单词背诵助手",
|
||||
"id": "29",
|
||||
"name": "英语单词背诵助手",
|
||||
"emoji": "📕",
|
||||
"group": "语言",
|
||||
"prompt": "- 版本:0.1\n- 语言:中文\n- 描述:您是一位语言专家,擅长阐释英语词汇的复杂性。您的角色是将复杂的英语单词分解为简单的概念,提供易懂的英语解释,提供中文翻译,并提供助记设备以帮助记忆。\n\n技能\n1. 分析高级英语单词的拼写、发音和含义。\n2. 使用简单的英语词汇进行解释,然后提供中文翻译。\n3. 使用音标联想、形象联想和词源等记忆技巧。\n4. 创作高质量的句子,以示范单词在语境中的使用。\n\n规则\n1. 总是以使用简单的英语词汇进行解释为开头。\n2. 在适当的时候,保持解释和例句的清晰、准确和幽默。\n3. 确保助记设备与记忆相关且有效。\n\n工作流程\n1. 问候用户并询问他们感兴趣的英语单词。\n2. 分解单词,分析其拼写、发音和复杂含义。\n3. 用简单的英语词汇解释,使含义更易理解。\n4. 提供单词的中文翻译和简单的英语解释。\n5. 针对单词的特点提供个性化的助记策略。\n6. 使用单词构建高质量、信息丰富且引人入胜的句子。\n\n初始化\n作为一名<角色>,您必须遵循<规则>并使用<语言>进行沟通。在问候用户时,确认他们想要理解和记忆的英语单词,然后按照<工作流程>进行操作。",
|
||||
"description": ""
|
||||
},
|
||||
{
|
||||
"id": 30,
|
||||
"name": "📖 文章总结 - Summarize",
|
||||
"id": "30",
|
||||
"name": "文章总结 - Summarize",
|
||||
"emoji": "📖",
|
||||
"group": "阅读",
|
||||
"prompt": "总结下面的文章,给出总结、摘要、观点三个部分内容,其中观点部分要使用列表列出,使用 Markdown 回复",
|
||||
@@ -1,5 +1,9 @@
|
||||
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'
|
||||
export const DEFAULT_MAX_TOKENS = 4096
|
||||
export const FONT_FAMILY =
|
||||
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
|
||||
export const platform = window.electron?.process?.platform
|
||||
export const isMac = platform === 'darwin'
|
||||
export const isWindows = platform === 'win32' || platform === 'win64'
|
||||
export const isLinux = platform === 'linux'
|
||||
|
||||
@@ -5,10 +5,17 @@ type SystemModel = Model & { enabled: boolean }
|
||||
export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
openai: [
|
||||
{
|
||||
id: 'gpt-3.5-turbo',
|
||||
id: 'gpt-4o',
|
||||
provider: 'openai',
|
||||
name: 'GPT-3.5 Turbo',
|
||||
group: 'GPT 3.5',
|
||||
name: ' GPT-4o',
|
||||
group: 'GPT 4o',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'gpt-4o-mini',
|
||||
provider: 'openai',
|
||||
name: ' GPT-4o-mini',
|
||||
group: 'GPT 4o',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
@@ -24,13 +31,6 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
||||
name: ' GPT-4',
|
||||
group: 'GPT 4',
|
||||
enabled: true
|
||||
},
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
provider: 'openai',
|
||||
name: ' GPT-4o',
|
||||
group: 'GPT 4o',
|
||||
enabled: true
|
||||
}
|
||||
],
|
||||
silicon: [
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.jpeg'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
|
||||
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg'
|
||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
|
||||
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg'
|
||||
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
|
||||
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
|
||||
import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png'
|
||||
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
|
||||
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
|
||||
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
|
||||
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
|
||||
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
|
||||
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
|
||||
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
|
||||
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
|
||||
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
|
||||
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
|
||||
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
|
||||
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
|
||||
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
|
||||
import YiModelLogo from '@renderer/assets/images/models/yi.svg'
|
||||
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
|
||||
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
|
||||
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
|
||||
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
|
||||
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
|
||||
import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.png'
|
||||
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
|
||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
||||
import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg'
|
||||
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpeg'
|
||||
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
|
||||
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
|
||||
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
|
||||
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
|
||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.jpeg'
|
||||
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
|
||||
|
||||
export function getProviderLogo(providerId: string) {
|
||||
switch (providerId) {
|
||||
@@ -107,7 +107,7 @@ export const PROVIDER_CONFIG = {
|
||||
},
|
||||
websites: {
|
||||
official: 'https://www.siliconflow.cn/',
|
||||
apiKey: 'https://cloud.siliconflow.cn/account/ak',
|
||||
apiKey: 'https://cloud.siliconflow.cn/account/ak?referrer=clxty1xuy0014lvqwh5z50i88',
|
||||
docs: 'https://docs.siliconflow.cn/',
|
||||
models: 'https://docs.siliconflow.cn/docs/model-names'
|
||||
}
|
||||
|
||||
2
src/renderer/src/env.d.ts
vendored
2
src/renderer/src/env.d.ts
vendored
@@ -1,8 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
import type KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import { MessageInstance } from 'antd/es/message/interface'
|
||||
import { HookAPI } from 'antd/es/modal/useModal'
|
||||
import type KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
||||
17
src/renderer/src/hooks/useAgents.ts
Normal file
17
src/renderer/src/hooks/useAgents.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { RootState } from '@renderer/store'
|
||||
import { addAgent, removeAgent, updateAgent, updateAgents } from '@renderer/store/agents'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { useDispatch, useSelector } from 'react-redux'
|
||||
|
||||
export function useAgents() {
|
||||
const agents = useSelector((state: RootState) => state.agents.agents)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
return {
|
||||
agents,
|
||||
addAgent: (agent: Agent) => dispatch(addAgent(agent)),
|
||||
removeAgent: (agent: Agent) => dispatch(removeAgent(agent)),
|
||||
updateAgent: (agent: Agent) => dispatch(updateAgent(agent)),
|
||||
updateAgents: (agents: Agent[]) => dispatch(updateAgents(agents))
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,22 @@
|
||||
import { i18nInit } from '@renderer/i18n'
|
||||
import i18n from '@renderer/i18n'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar } from '@renderer/store/runtime'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { useSettings } from './useSettings'
|
||||
|
||||
export function useAppInit() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { proxyUrl } = useSettings()
|
||||
const { language } = useSettings()
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
const storedImage = await LocalStorage.getImage('avatar')
|
||||
storedImage && dispatch(setAvatar(storedImage))
|
||||
})
|
||||
i18nInit()
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -28,4 +29,8 @@ export function useAppInit() {
|
||||
useEffect(() => {
|
||||
proxyUrl && window.api.setProxy(proxyUrl)
|
||||
}, [proxyUrl])
|
||||
|
||||
useEffect(() => {
|
||||
i18n.changeLanguage(language || navigator.language || 'en-US')
|
||||
}, [language])
|
||||
}
|
||||
|
||||
18
src/renderer/src/hooks/useOllama.ts
Normal file
18
src/renderer/src/hooks/useOllama.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { setOllamaKeepAliveTime } from '@renderer/store/llm'
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
export function useOllamaSettings() {
|
||||
const settings = useAppSelector((state) => state.llm.settings.ollama)
|
||||
const dispatch = useDispatch()
|
||||
|
||||
return { ...settings, setKeepAliveTime: (time: number) => dispatch(setOllamaKeepAliveTime(time)) }
|
||||
}
|
||||
|
||||
export function getOllamaSettings() {
|
||||
return store.getState().llm.settings.ollama
|
||||
}
|
||||
|
||||
export function getOllamaKeepAliveTime() {
|
||||
return store.getState().llm.settings.ollama.keepAliveTime + 'm'
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
addModel,
|
||||
@@ -8,8 +9,8 @@ import {
|
||||
updateProviders
|
||||
} from '@renderer/store/llm'
|
||||
import { Assistant, Model, Provider } from '@renderer/types'
|
||||
|
||||
import { useDefaultModel } from './useAssistant'
|
||||
import { createSelector } from '@reduxjs/toolkit'
|
||||
|
||||
const selectEnabledProviders = createSelector(
|
||||
(state) => state.llm.providers,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
SendMessageShortcut,
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
setTheme,
|
||||
ThemeMode
|
||||
} from '@renderer/store/settings'
|
||||
|
||||
@@ -2,12 +2,10 @@ import { Assistant, Topic } from '@renderer/types'
|
||||
import { find } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const activeTopicsMap = new Map<string, Topic>()
|
||||
let _activeTopic: Topic
|
||||
|
||||
export function useActiveTopic(assistant: Assistant) {
|
||||
const [activeTopic, setActiveTopic] = useState(activeTopicsMap.get(assistant.id) || assistant?.topics[0])
|
||||
|
||||
activeTopicsMap.set(assistant.id, activeTopic)
|
||||
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
|
||||
|
||||
useEffect(() => {
|
||||
// activeTopic not in assistant.topics
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import store from '@renderer/store'
|
||||
import i18n from 'i18next'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
|
||||
@@ -26,7 +25,9 @@ const resources = {
|
||||
provider: 'Provider',
|
||||
you: 'You',
|
||||
save: 'Save',
|
||||
footnotes: 'Footnotes'
|
||||
footnotes: 'References',
|
||||
select: 'Select',
|
||||
search: 'Search'
|
||||
},
|
||||
button: {
|
||||
add: 'Add',
|
||||
@@ -49,10 +50,8 @@ const resources = {
|
||||
'switch.disabled': 'Switching is disabled while the assistant is generating'
|
||||
},
|
||||
chat: {
|
||||
save: 'Save'
|
||||
},
|
||||
assistant: {
|
||||
'default.name': '😀 Default Assistant',
|
||||
save: 'Save',
|
||||
'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',
|
||||
@@ -61,7 +60,7 @@ const resources = {
|
||||
'topics.edit.placeholder': 'Enter new name',
|
||||
'topics.delete.all.title': 'Delete all topics',
|
||||
'topics.delete.all.content': 'Are you sure you want to delete all topics?',
|
||||
'input.new_chat': ' New Chat ',
|
||||
'input.new_topic': 'New Topic',
|
||||
'input.topics': ' Topics ',
|
||||
'input.clear': 'Clear',
|
||||
'input.expand': 'Expand',
|
||||
@@ -72,18 +71,37 @@ const resources = {
|
||||
'input.send': 'Send',
|
||||
'input.pause': 'Pause',
|
||||
'input.settings': 'Settings',
|
||||
'input.context_count.tip': 'Context Count',
|
||||
'input.estimated_tokens.tip': 'Estimated tokens',
|
||||
'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.max_tokens': 'Enable Max Tokens Limit',
|
||||
'settings.max_tokens.tip':
|
||||
'The maximum number of tokens the model can generate. Normal chat suggests 500-800. Short text generation suggests 800-2000. Code generation suggests 2000-3600. Long text generation suggests above 4000.',
|
||||
'settings.reset': 'Reset',
|
||||
'settings.set_as_default': 'Apply to default assistant',
|
||||
'settings.max': 'Max',
|
||||
'suggestions.title': 'Suggested Questions'
|
||||
'suggestions.title': 'Suggested Questions',
|
||||
'add.assistant.title': 'Add Assistant'
|
||||
},
|
||||
apps: {
|
||||
title: 'Agents'
|
||||
agents: {
|
||||
title: 'Agents',
|
||||
my_agents: 'My Agents',
|
||||
'add.title': 'Add Agent',
|
||||
'edit.title': 'Edit Agent',
|
||||
'add.name': 'Name',
|
||||
'add.name.placeholder': 'Enter name',
|
||||
'add.prompt': 'Prompt',
|
||||
'add.prompt.placeholder': 'Enter prompt',
|
||||
'add.button': 'Add',
|
||||
'manage.title': 'Manage Agents',
|
||||
'delete.popup.content': 'Are you sure you want to delete this agent?',
|
||||
'tag.default': 'Default',
|
||||
'tag.system': 'System',
|
||||
'tag.user': 'Mine'
|
||||
},
|
||||
provider: {
|
||||
openai: 'OpenAI',
|
||||
@@ -113,6 +131,7 @@ const resources = {
|
||||
'messages.use_serif_font': 'Use serif font',
|
||||
'messages.input.title': 'Input Settings',
|
||||
'messages.input.show_estimated_tokens': 'Show estimated input tokens',
|
||||
'messages.input.send_shortcuts': 'Send shortcuts',
|
||||
'general.title': 'General Settings',
|
||||
'general.user_name': 'User Name',
|
||||
'general.user_name.placeholder': 'Enter your name',
|
||||
@@ -186,6 +205,15 @@ const resources = {
|
||||
italian: 'Italian',
|
||||
portuguese: 'Portuguese',
|
||||
arabic: 'Arabic'
|
||||
},
|
||||
ollama: {
|
||||
title: 'Ollama',
|
||||
'keep_alive_time.title': 'Keep Alive Time',
|
||||
'keep_alive_time.placeholder': 'Minutes',
|
||||
'keep_alive_time.description': 'The time in minutes to keep the connection alive, default is 5 minutes.'
|
||||
},
|
||||
error: {
|
||||
'chat.response': 'Something went wrong. Please check if you have set your API key in the Settings > Providers'
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -211,7 +239,9 @@ const resources = {
|
||||
regenerate: '重新生成',
|
||||
provider: '提供商',
|
||||
you: '用户',
|
||||
footnote: '引用内容'
|
||||
footnote: '引用内容',
|
||||
select: '选择',
|
||||
search: '搜索'
|
||||
},
|
||||
button: {
|
||||
add: '添加',
|
||||
@@ -234,10 +264,8 @@ const resources = {
|
||||
'switch.disabled': '模型回复完成后才能切换'
|
||||
},
|
||||
chat: {
|
||||
save: '保存'
|
||||
},
|
||||
assistant: {
|
||||
'default.name': '😃 默认助手 - Assistant',
|
||||
save: '保存',
|
||||
'default.name': '🔆 默认助手 - Assistant',
|
||||
'default.description': '你好,我是默认助手。你可以立刻开始跟我聊天。',
|
||||
'default.topic.name': '默认话题',
|
||||
'topics.title': '话题',
|
||||
@@ -246,7 +274,7 @@ const resources = {
|
||||
'topics.edit.placeholder': '输入新名称',
|
||||
'topics.delete.all.title': '删除所有话题',
|
||||
'topics.delete.all.content': '确定要删除所有话题吗?',
|
||||
'input.new_chat': ' 新聊天 ',
|
||||
'input.new_topic': '新话题',
|
||||
'input.topics': ' 话题 ',
|
||||
'input.clear': '清除',
|
||||
'input.expand': '展开',
|
||||
@@ -257,19 +285,38 @@ const resources = {
|
||||
'input.send': '发送',
|
||||
'input.pause': '暂停',
|
||||
'input.settings': '设置',
|
||||
'input.context_count.tip': '上下文数',
|
||||
'input.estimated_tokens.tip': '预估 token 数',
|
||||
'settings.temperature': '模型温度',
|
||||
'settings.temperature.tip':
|
||||
'模型生成文本的随机程度。值越大,回复内容越赋有多样性、创造性、随机性;设为 0 根据事实回答。日常聊天建议设置为 0.7',
|
||||
'settings.conext_count': '上下文数',
|
||||
'settings.conext_count.tip':
|
||||
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10,代码生成建议 5-10',
|
||||
'要保留在上下文中的消息数量,数值越大,上下文越长,消耗的 token 越多。普通聊天建议 5-10',
|
||||
'settings.max_tokens': '开启消息长度限制',
|
||||
'settings.max_tokens.tip':
|
||||
'单次交互所用的最大 Token 数, 会影响返回结果的长度。普通聊天建议 500-800;短文生成建议 800-2000;代码生成建议 2000-3600;长文生成建议切换模型到 4000 左右',
|
||||
'settings.reset': '重置',
|
||||
'settings.set_as_default': '应用到默认助手',
|
||||
'settings.max': '不限',
|
||||
'suggestions.title': '建议的问题'
|
||||
'suggestions.title': '建议的问题',
|
||||
'add.assistant.title': '添加智能体'
|
||||
},
|
||||
apps: {
|
||||
title: '智能体'
|
||||
agents: {
|
||||
title: '智能体',
|
||||
my_agents: '我的智能体',
|
||||
'add.title': '添加智能体',
|
||||
'edit.title': '编辑智能体',
|
||||
'add.name': '名称',
|
||||
'add.name.placeholder': '输入名称',
|
||||
'add.prompt': '提示词',
|
||||
'add.prompt.placeholder': '输入提示词',
|
||||
'add.button': '添加',
|
||||
'manage.title': '管理智能体',
|
||||
'delete.popup.content': '确定要删除此智能体吗?',
|
||||
'tag.default': '默认',
|
||||
'tag.system': '系统',
|
||||
'tag.user': '我的'
|
||||
},
|
||||
provider: {
|
||||
openai: 'OpenAI',
|
||||
@@ -299,6 +346,7 @@ const resources = {
|
||||
'messages.use_serif_font': '使用衬线字体',
|
||||
'messages.input.title': '输入设置',
|
||||
'messages.input.show_estimated_tokens': '状态显示',
|
||||
'messages.input.send_shortcuts': '发送快捷键',
|
||||
'general.title': '常规设置',
|
||||
'general.user_name': '用户名',
|
||||
'general.user_name.placeholder': '请输入用户名',
|
||||
@@ -372,6 +420,15 @@ const resources = {
|
||||
italian: '意大利文',
|
||||
portuguese: '葡萄牙文',
|
||||
arabic: '阿拉伯文'
|
||||
},
|
||||
ollama: {
|
||||
title: 'Ollama',
|
||||
'keep_alive_time.title': '保持活跃时间',
|
||||
'keep_alive_time.placeholder': '分钟',
|
||||
'keep_alive_time.description': '对话后模型在内存中保持的时间(默认:5分钟)'
|
||||
},
|
||||
error: {
|
||||
'chat.response': '出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -386,8 +443,4 @@ i18n.use(initReactI18next).init({
|
||||
}
|
||||
})
|
||||
|
||||
export function i18nInit() {
|
||||
i18n.changeLanguage(store.getState().settings.language || 'en-US')
|
||||
}
|
||||
|
||||
export default i18n
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import localforage from 'localforage'
|
||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import * as Sentry from '@sentry/electron/renderer'
|
||||
import { isProduction, loadScript } from './utils'
|
||||
import localforage from 'localforage'
|
||||
|
||||
import { ThemeMode } from './store/settings'
|
||||
import { isProduction, loadScript } from './utils'
|
||||
|
||||
async function initSentry() {
|
||||
if (await isProduction()) {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './assets/styles/index.scss'
|
||||
import './init'
|
||||
import './i18n'
|
||||
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
||||
120
src/renderer/src/pages/agents/AgentsPage.tsx
Normal file
120
src/renderer/src/pages/agents/AgentsPage.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { UnorderedListOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import Agents from '@renderer/config/agents.json'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { covertAgentToAssistant } from '@renderer/services/assistant'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { Col, Row, Typography } from 'antd'
|
||||
import { find, groupBy } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AgentCard from './components/AgentCard'
|
||||
import ManageAgentsPopup from './components/ManageAgentsPopup'
|
||||
import UserAgents from './components/UserAgents'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const AppsPage: FC = () => {
|
||||
const { assistants, addAssistant } = useAssistants()
|
||||
const { agents } = useAgents()
|
||||
const agentGroups = groupBy(Agents, 'group')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onAddAgentConfirm = (agent: Agent) => {
|
||||
const added = find(assistants, { id: agent.id })
|
||||
|
||||
window.modal.confirm({
|
||||
title: agent.emoji + ' ' + agent.name,
|
||||
content: agent.description || agent.prompt,
|
||||
icon: null,
|
||||
closable: true,
|
||||
maskClosable: true,
|
||||
okButtonProps: { type: 'primary', disabled: Boolean(added) },
|
||||
okText: added ? t('button.added') : t('button.add'),
|
||||
onOk: () => onAddAgent(agent)
|
||||
})
|
||||
}
|
||||
|
||||
const onAddAgent = (agent: Agent) => {
|
||||
addAssistant(covertAgentToAssistant(agent))
|
||||
window.message.success({
|
||||
content: t('message.assistant.added.content'),
|
||||
key: 'agent-added',
|
||||
style: { marginTop: '5vh' }
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('agents.title')}</NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer>
|
||||
<AssistantsContainer>
|
||||
<HStack alignItems="center" style={{ marginBottom: 16 }}>
|
||||
<Title level={3}>{t('agents.my_agents')}</Title>
|
||||
{agents.length > 0 && <ManageIcon onClick={ManageAgentsPopup.show} />}
|
||||
</HStack>
|
||||
<UserAgents onAdd={onAddAgentConfirm} />
|
||||
{Object.keys(agentGroups).map((group) => (
|
||||
<div key={group}>
|
||||
<Title level={3} key={group} style={{ marginBottom: 16 }}>
|
||||
{group}
|
||||
</Title>
|
||||
<Row gutter={16}>
|
||||
{agentGroups[group].map((agent, index) => {
|
||||
return (
|
||||
<Col span={8} key={group + index}>
|
||||
<AgentCard onClick={() => onAddAgentConfirm(agent)} agent={agent as any} />
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ minHeight: 20 }} />
|
||||
</AssistantsContainer>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
const AssistantsContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 20px;
|
||||
max-width: 1000px;
|
||||
`
|
||||
|
||||
const ManageIcon = styled(UnorderedListOutlined)`
|
||||
font-size: 18px;
|
||||
color: var(--color-icon);
|
||||
cursor: pointer;
|
||||
margin-bottom: 0.5em;
|
||||
margin-left: 0.5em;
|
||||
`
|
||||
|
||||
export default AppsPage
|
||||
137
src/renderer/src/pages/agents/components/AddAgentPopup.tsx
Normal file
137
src/renderer/src/pages/agents/components/AddAgentPopup.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import 'emoji-picker-element'
|
||||
|
||||
import EmojiPicker from '@renderer/components/EmojiPicker'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { syncAgentToAssistant } from '@renderer/services/assistant'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { getLeadingEmoji, uuid } from '@renderer/utils'
|
||||
import { Button, Form, FormInstance, Input, Modal, Popover } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
agent?: Agent
|
||||
resolve: (data: Agent | null) => void
|
||||
}
|
||||
|
||||
type FieldType = {
|
||||
id: string
|
||||
name: string
|
||||
prompt: string
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ agent, resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const [form] = Form.useForm()
|
||||
const { t } = useTranslation()
|
||||
const { addAgent, updateAgent } = useAgents()
|
||||
const formRef = useRef<FormInstance>(null)
|
||||
const [emoji, setEmoji] = useState(agent?.emoji)
|
||||
|
||||
const onFinish = (values: FieldType) => {
|
||||
const _emoji = emoji || getLeadingEmoji(values.name)
|
||||
|
||||
if (values.name.trim() === '' || values.prompt.trim() === '') {
|
||||
return
|
||||
}
|
||||
|
||||
if (agent) {
|
||||
const _agent = {
|
||||
...agent,
|
||||
name: values.name,
|
||||
emoji: _emoji,
|
||||
prompt: values.prompt
|
||||
}
|
||||
updateAgent(_agent)
|
||||
syncAgentToAssistant(_agent)
|
||||
resolve(_agent)
|
||||
setOpen(false)
|
||||
return
|
||||
}
|
||||
|
||||
const _agent = {
|
||||
id: uuid(),
|
||||
name: values.name,
|
||||
emoji: _emoji,
|
||||
prompt: values.prompt,
|
||||
group: 'user'
|
||||
}
|
||||
|
||||
addAgent(_agent)
|
||||
resolve(_agent)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (agent) {
|
||||
form.setFieldsValue({
|
||||
name: agent.name,
|
||||
prompt: agent.prompt
|
||||
})
|
||||
}
|
||||
}, [agent, form])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
style={{ marginTop: '10vh' }}
|
||||
title={agent ? t('agents.edit.title') : t('agents.add.title')}
|
||||
open={open}
|
||||
onOk={() => formRef.current?.submit()}
|
||||
onCancel={onCancel}
|
||||
maskClosable={false}
|
||||
afterClose={onClose}
|
||||
okText={agent ? t('common.save') : t('agents.add.button')}>
|
||||
<Form
|
||||
ref={formRef}
|
||||
form={form}
|
||||
labelCol={{ flex: '80px' }}
|
||||
labelAlign="left"
|
||||
colon={false}
|
||||
style={{ marginTop: 25 }}
|
||||
onFinish={onFinish}>
|
||||
<Form.Item name="name" label="Emoji">
|
||||
<Popover content={<EmojiPicker onEmojiClick={setEmoji} />} trigger="click" arrow>
|
||||
<Button icon={emoji && <span style={{ fontSize: 20 }}>{emoji}</span>}>{t('common.select')}</Button>
|
||||
</Popover>
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label={t('agents.add.name')} rules={[{ required: true }]}>
|
||||
<Input placeholder={t('agents.add.name.placeholder')} spellCheck={false} allowClear />
|
||||
</Form.Item>
|
||||
<Form.Item name="prompt" label={t('agents.add.prompt')} rules={[{ required: true }]}>
|
||||
<TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={4} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default class AddAgentPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide('AddAgentPopup')
|
||||
}
|
||||
static show(agent?: Agent) {
|
||||
return new Promise<Agent | null>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
agent={agent}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>,
|
||||
'AddAgentPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
81
src/renderer/src/pages/agents/components/AgentCard.tsx
Normal file
81
src/renderer/src/pages/agents/components/AgentCard.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Agent } from '@renderer/types'
|
||||
import { Col, Typography } from 'antd'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
agent: Agent
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const AgentCard: React.FC<Props> = ({ agent, onClick }) => {
|
||||
return (
|
||||
<Container onClick={onClick}>
|
||||
{agent.emoji && <EmojiHeader>{agent.emoji}</EmojiHeader>}
|
||||
<Col>
|
||||
<AgentHeader>
|
||||
<AgentName level={5} style={{ marginBottom: 0 }}>
|
||||
{agent.name}
|
||||
</AgentName>
|
||||
</AgentHeader>
|
||||
<AgentCardPrompt>{agent.prompt}</AgentCardPrompt>
|
||||
</Col>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 16px;
|
||||
background-color: var(--color-background-soft);
|
||||
border: 0.5px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
const EmojiHeader = styled.div`
|
||||
width: 25px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
font-size: 25px;
|
||||
line-height: 25px;
|
||||
`
|
||||
|
||||
const AgentHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const AgentName = styled(Title)`
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
color: var(--color-white);
|
||||
font-weight: 900;
|
||||
`
|
||||
|
||||
const AgentCardPrompt = styled.div`
|
||||
color: #666;
|
||||
margin-top: 6px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
export default AgentCard
|
||||
109
src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx
Normal file
109
src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { DeleteOutlined, EditOutlined, MenuOutlined } from '@ant-design/icons'
|
||||
import DragableList from '@renderer/components/DragableList'
|
||||
import { Box, HStack } from '@renderer/components/Layout'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { Empty, Modal, Popconfirm } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddAgentPopup from './AddAgentPopup'
|
||||
|
||||
const PopupContainer: React.FC = () => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { t } = useTranslation()
|
||||
const { agents, removeAgent, updateAgents } = useAgents()
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = async () => {
|
||||
ManageAgentsPopup.hide()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (agents.length === 0) {
|
||||
setOpen(false)
|
||||
}
|
||||
}, [agents])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
style={{ marginTop: '10vh' }}
|
||||
title={t('agents.manage.title')}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
footer={null}>
|
||||
<Container>
|
||||
{agents.length > 0 && (
|
||||
<DragableList list={agents} onUpdate={updateAgents}>
|
||||
{(item) => (
|
||||
<AgentItem>
|
||||
<Box mr={8}>
|
||||
{item.emoji} {item.name}
|
||||
</Box>
|
||||
<HStack gap="15px">
|
||||
<Popconfirm
|
||||
title={t('agents.delete.popup.content')}
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => removeAgent(item)}>
|
||||
<DeleteOutlined style={{ color: 'var(--color-error)' }} />
|
||||
</Popconfirm>
|
||||
<EditOutlined style={{ cursor: 'pointer' }} onClick={() => AddAgentPopup.show(item)} />
|
||||
<MenuOutlined style={{ cursor: 'move' }} />
|
||||
</HStack>
|
||||
</AgentItem>
|
||||
)}
|
||||
</DragableList>
|
||||
)}
|
||||
{agents.length === 0 && <Empty description="" />}
|
||||
</Container>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 12px 0;
|
||||
height: 50vh;
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const AgentItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
background-color: var(--color-background-soft);
|
||||
margin-bottom: 8px;
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
color: var(--color-icon);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
|
||||
export default class ManageAgentsPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide('ManageAgentsPopup')
|
||||
}
|
||||
static show() {
|
||||
TopView.show(<PopupContainer />, 'ManageAgentsPopup')
|
||||
}
|
||||
}
|
||||
57
src/renderer/src/pages/agents/components/UserAgents.tsx
Normal file
57
src/renderer/src/pages/agents/components/UserAgents.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { Col, Row } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddAssistantPopup from './AddAgentPopup'
|
||||
import AgentCard from './AgentCard'
|
||||
|
||||
interface Props {
|
||||
onAdd: (agent: Agent) => void
|
||||
}
|
||||
|
||||
const UserAgents: FC<Props> = ({ onAdd }) => {
|
||||
const { agents } = useAgents()
|
||||
|
||||
const onAddMyAgentClick = () => {
|
||||
AddAssistantPopup.show()
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
{agents.map((agent) => (
|
||||
<Col span={8} key={agent.id}>
|
||||
<AgentCard agent={agent} onClick={() => onAdd(agent)} />
|
||||
</Col>
|
||||
))}
|
||||
<Col span={8}>
|
||||
<AssistantCardContainer style={{ borderStyle: 'dashed' }} onClick={onAddMyAgentClick}>
|
||||
<PlusOutlined />
|
||||
</AssistantCardContainer>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
const AssistantCardContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
border: 1px dashed var(--color-border);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
min-height: 84px;
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
color: var(--color-icon);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
`
|
||||
|
||||
export default UserAgents
|
||||
@@ -1,169 +0,0 @@
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { Col, Row, Typography } from 'antd'
|
||||
import { find, groupBy } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { SystemAssistant } from '@renderer/types'
|
||||
import { getDefaultAssistant } from '@renderer/services/assistant'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import SYSTEM_ASSISTANTS from '@renderer/config/assistants.json'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
const AppsPage: FC = () => {
|
||||
const { assistants, addAssistant } = useAssistants()
|
||||
const assistantGroups = groupBy(
|
||||
SYSTEM_ASSISTANTS.map((a) => ({ ...a, id: String(a.id) })),
|
||||
'group'
|
||||
)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onAddAssistantConfirm = (assistant: SystemAssistant) => {
|
||||
const added = find(assistants, { id: assistant.id })
|
||||
|
||||
window.modal.confirm({
|
||||
title: assistant.name,
|
||||
content: assistant.description || assistant.prompt,
|
||||
icon: null,
|
||||
closable: true,
|
||||
maskClosable: true,
|
||||
okButtonProps: { type: 'primary', disabled: Boolean(added) },
|
||||
okText: added ? t('button.added') : t('button.add'),
|
||||
onOk: () => onAddAssistant(assistant)
|
||||
})
|
||||
}
|
||||
|
||||
const onAddAssistant = (assistant: SystemAssistant) => {
|
||||
addAssistant({
|
||||
...getDefaultAssistant(),
|
||||
...assistant,
|
||||
id: String(assistant.id)
|
||||
})
|
||||
window.message.success({
|
||||
content: t('message.assistant.added.content'),
|
||||
key: 'assistant-added',
|
||||
style: { marginTop: '5vh' }
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('apps.title')}</NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer>
|
||||
<AssistantsContainer>
|
||||
{Object.keys(assistantGroups).map((group) => (
|
||||
<div key={group}>
|
||||
<Title level={3} key={group} style={{ marginBottom: 16 }}>
|
||||
{group}
|
||||
</Title>
|
||||
<Row gutter={16}>
|
||||
{assistantGroups[group].map((assistant, index) => {
|
||||
return (
|
||||
<Col span={8} key={group + index}>
|
||||
<AssistantCard onClick={() => onAddAssistantConfirm(assistant)}>
|
||||
<EmojiHeader>{assistant.emoji}</EmojiHeader>
|
||||
<Col>
|
||||
<AssistantHeader>
|
||||
<AssistantName level={5} style={{ marginBottom: 0 }}>
|
||||
{assistant.name.replace(assistant.emoji + ' ', '')}
|
||||
</AssistantName>
|
||||
</AssistantHeader>
|
||||
<AssistantCardPrompt>{assistant.prompt}</AssistantCardPrompt>
|
||||
</Col>
|
||||
</AssistantCard>
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</Row>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ minHeight: 20 }} />
|
||||
</AssistantsContainer>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
`
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
background-color: var(--color-background);
|
||||
`
|
||||
|
||||
const AssistantsContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 20px;
|
||||
max-width: 1000px;
|
||||
`
|
||||
|
||||
const AssistantCard = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 16px;
|
||||
background-color: var(--color-background-soft);
|
||||
border: 0.5px solid var(--color-border);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
const EmojiHeader = styled.div`
|
||||
width: 25px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 5px;
|
||||
font-size: 25px;
|
||||
line-height: 25px;
|
||||
`
|
||||
|
||||
const AssistantHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const AssistantName = styled(Title)`
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
color: var(--color-white);
|
||||
font-weight: 900;
|
||||
`
|
||||
|
||||
const AssistantCardPrompt = styled.div`
|
||||
color: #666;
|
||||
margin-top: 6px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
export default AppsPage
|
||||
@@ -1,16 +1,18 @@
|
||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Switch } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Chat from './components/Chat'
|
||||
|
||||
import AddAssistantPopup from './components/AddAssistantPopup'
|
||||
import Assistants from './components/Assistants'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import Chat from './components/Chat'
|
||||
import Navigation from './components/NavigationCenter'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { Switch } from 'antd'
|
||||
|
||||
let _activeAssistant: Assistant
|
||||
|
||||
@@ -24,12 +26,17 @@ const HomePage: FC = () => {
|
||||
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
const onCreateAssistant = () => {
|
||||
const onCreateDefaultAssistant = () => {
|
||||
const assistant = { ...defaultAssistant, id: uuid() }
|
||||
addAssistant(assistant)
|
||||
setActiveAssistant(assistant)
|
||||
}
|
||||
|
||||
const onCreateAssistant = async () => {
|
||||
const assistant = await AddAssistantPopup.show()
|
||||
assistant && setActiveAssistant(assistant)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Navbar>
|
||||
@@ -61,7 +68,7 @@ const HomePage: FC = () => {
|
||||
<Assistants
|
||||
activeAssistant={activeAssistant}
|
||||
setActiveAssistant={setActiveAssistant}
|
||||
onCreateAssistant={onCreateAssistant}
|
||||
onCreateAssistant={onCreateDefaultAssistant}
|
||||
/>
|
||||
)}
|
||||
<Chat assistant={activeAssistant} />
|
||||
|
||||
129
src/renderer/src/pages/home/components/AddAssistantPopup.tsx
Normal file
129
src/renderer/src/pages/home/components/AddAssistantPopup.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import systemAgents from '@renderer/config/agents.json'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { covertAgentToAssistant } from '@renderer/services/assistant'
|
||||
import { Agent, Assistant } from '@renderer/types'
|
||||
import { Input, Modal, Tag } from 'antd'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
resolve: (value: Assistant | undefined) => void
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { t } = useTranslation()
|
||||
const { agents: userAgents } = useAgents()
|
||||
const [searchText, setSearchText] = useState('')
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { assistants, addAssistant } = useAssistants()
|
||||
|
||||
const defaultAgent: Agent = useMemo(
|
||||
() => ({
|
||||
id: defaultAssistant.id,
|
||||
name: defaultAssistant.name,
|
||||
emoji: '',
|
||||
prompt: defaultAssistant.prompt,
|
||||
group: 'system'
|
||||
}),
|
||||
[defaultAssistant.id, defaultAssistant.name, defaultAssistant.prompt]
|
||||
)
|
||||
|
||||
const agents = useMemo(() => {
|
||||
const allAgents = [defaultAgent, ...userAgents, ...systemAgents] as Agent[]
|
||||
const list = allAgents.filter((agent) => !assistants.map((a) => a.id).includes(agent.id))
|
||||
return searchText
|
||||
? list.filter((agent) => agent.name.toLowerCase().includes(searchText.trim().toLocaleLowerCase()))
|
||||
: list
|
||||
}, [assistants, defaultAgent, searchText, userAgents])
|
||||
|
||||
const onCreateAssistant = (agent: Agent) => {
|
||||
if (assistants.map((a) => a.id).includes(String(agent.id))) return
|
||||
const assistant = covertAgentToAssistant(agent)
|
||||
addAssistant(assistant)
|
||||
resolve(assistant)
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = async () => {
|
||||
resolve(undefined)
|
||||
AddAssistantPopup.hide()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
style={{ marginTop: '5vh' }}
|
||||
title={t('chat.add.assistant.title')}
|
||||
open={open}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
transitionName=""
|
||||
maskTransitionName=""
|
||||
footer={null}>
|
||||
<Input
|
||||
placeholder={t('common.search')}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
autoFocus
|
||||
style={{ marginBottom: 16 }}
|
||||
/>
|
||||
<Container>
|
||||
{agents.map((agent) => (
|
||||
<AgentItem key={agent.id} onClick={() => onCreateAssistant(agent)}>
|
||||
{agent.emoji} {agent.name}
|
||||
{agent.group === 'system' && <Tag color="orange">{t('agents.tag.system')}</Tag>}
|
||||
{agent.group === 'user' && <Tag color="green">{t('agents.tag.user')}</Tag>}
|
||||
</AgentItem>
|
||||
))}
|
||||
</Container>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
height: 50vh;
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const AgentItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
background-color: var(--color-background-soft);
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
color: var(--color-icon);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
|
||||
export default class AddAssistantPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide('AddAssistantPopup')
|
||||
}
|
||||
static show() {
|
||||
return new Promise<Assistant | undefined>((resolve) => {
|
||||
TopView.show(<PopupContainer resolve={resolve} />, 'AddAssistantPopup')
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ 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 { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||
import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { Assistant } from '@renderer/types'
|
||||
@@ -10,7 +10,7 @@ import { droppableReorder, uuid } from '@renderer/utils'
|
||||
import { Dropdown } from 'antd'
|
||||
import { ItemType } from 'antd/es/menu/interface'
|
||||
import { last } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
import { FC, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -22,65 +22,79 @@ interface Props {
|
||||
|
||||
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
|
||||
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
|
||||
const { updateAssistant } = useAssistant(activeAssistant.id)
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
|
||||
const { updateAssistant } = useAssistant(activeAssistant.id)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onDelete = (assistant: Assistant) => {
|
||||
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
|
||||
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant()
|
||||
removeAssistant(assistant.id)
|
||||
}
|
||||
const onDelete = useCallback(
|
||||
(assistant: Assistant) => {
|
||||
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
|
||||
_assistant ? setActiveAssistant(_assistant) : onCreateAssistant()
|
||||
removeAssistant(assistant.id)
|
||||
},
|
||||
[assistants, onCreateAssistant, removeAssistant, setActiveAssistant]
|
||||
)
|
||||
|
||||
const getMenuItems = (assistant: Assistant) =>
|
||||
[
|
||||
{
|
||||
label: t('common.edit'),
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const _assistant = await AssistantSettingPopup.show({ assistant })
|
||||
updateAssistant(_assistant)
|
||||
const getMenuItems = useCallback(
|
||||
(assistant: Assistant) =>
|
||||
[
|
||||
{
|
||||
label: t('common.edit'),
|
||||
key: 'edit',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const _assistant = await AssistantSettingPopup.show({ assistant })
|
||||
updateAssistant(_assistant)
|
||||
syncAsistantToAgent(_assistant)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common.duplicate'),
|
||||
key: 'duplicate',
|
||||
icon: <CopyOutlined />,
|
||||
onClick: async () => {
|
||||
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
|
||||
addAssistant(_assistant)
|
||||
setActiveAssistant(_assistant)
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: t('common.delete'),
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => onDelete(assistant)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('common.duplicate'),
|
||||
key: 'duplicate',
|
||||
icon: <CopyOutlined />,
|
||||
onClick: async () => {
|
||||
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
|
||||
addAssistant(_assistant)
|
||||
setActiveAssistant(_assistant)
|
||||
}
|
||||
},
|
||||
{ type: 'divider' },
|
||||
{
|
||||
label: t('common.delete'),
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => onDelete(assistant)
|
||||
] as ItemType[],
|
||||
[addAssistant, onDelete, setActiveAssistant, t, updateAssistant]
|
||||
)
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
(result: DropResult) => {
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
const reorderAssistants = droppableReorder<Assistant>(assistants, sourceIndex, destIndex)
|
||||
updateAssistants(reorderAssistants)
|
||||
}
|
||||
] as ItemType[]
|
||||
},
|
||||
[assistants, updateAssistants]
|
||||
)
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
const reorderAssistants = droppableReorder<Assistant>(assistants, sourceIndex, destIndex)
|
||||
updateAssistants(reorderAssistants)
|
||||
}
|
||||
}
|
||||
|
||||
const onSwitchAssistant = (assistant: Assistant) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
|
||||
setActiveAssistant(assistant)
|
||||
}
|
||||
const onSwitchAssistant = useCallback(
|
||||
(assistant: Assistant): any => {
|
||||
if (generating) {
|
||||
return window.message.warning({
|
||||
content: t('message.switch.disabled'),
|
||||
key: 'switch-assistant'
|
||||
})
|
||||
}
|
||||
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
|
||||
setActiveAssistant(assistant)
|
||||
},
|
||||
[generating, setActiveAssistant, t]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -91,14 +105,16 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
||||
{assistants.map((assistant, index) => (
|
||||
<Draggable key={`draggable_${assistant.id}_${index}`} draggableId={assistant.id} index={index}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
|
||||
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
|
||||
<AssistantItem
|
||||
onClick={() => onSwitchAssistant(assistant)}
|
||||
className={assistant.id === activeAssistant?.id ? 'active' : ''}>
|
||||
<AssistantName className="name">
|
||||
{assistant.name || t('assistant.default.name')}
|
||||
</AssistantName>
|
||||
<AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName>
|
||||
</AssistantItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
@@ -129,10 +145,9 @@ const AssistantItem = styled.div`
|
||||
flex-direction: column;
|
||||
padding: 7px 10px;
|
||||
position: relative;
|
||||
border-radius: 3px;
|
||||
margin-bottom: 5px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-family: Poppins;
|
||||
font-family: Ubuntu;
|
||||
.anticon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import Inputbar from './Inputbar'
|
||||
import Messages from './Messages'
|
||||
import { Flex } from 'antd'
|
||||
import RightSidebar from './RightSidebar'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Flex } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Inputbar from './input/Inputbar'
|
||||
import Messages from './Messages'
|
||||
import RightSidebar from './sidebar/RightSidebar'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
import {
|
||||
CheckOutlined,
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
MenuOutlined,
|
||||
QuestionCircleOutlined,
|
||||
SaveOutlined,
|
||||
SyncOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { FONT_FAMILY } from '@renderer/config/constant'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useRuntime } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Message } from '@renderer/types'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
|
||||
import { Avatar, Dropdown, Tooltip } from 'antd'
|
||||
import { Alert, Avatar, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { upperFirst } from 'lodash'
|
||||
import { FC, useCallback, useMemo, useState } from 'react'
|
||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Markdown from './markdown/Markdown'
|
||||
import SelectModelDropdown from './SelectModelDropdown'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@@ -34,14 +37,16 @@ interface Props {
|
||||
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
|
||||
const avatar = useAvatar()
|
||||
const { t } = useTranslation()
|
||||
const { assistant } = useAssistant(message.assistantId)
|
||||
const { assistant, model, setModel } = useAssistant(message.assistantId)
|
||||
const { userName, showMessageDivider, messageFont } = useSettings()
|
||||
const { generating } = useRuntime()
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const isLastMessage = index === 0
|
||||
const isUserMessage = message.role === 'user'
|
||||
const canRegenerate = isLastMessage && message.role === 'assistant'
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
const canRegenerate = isLastMessage && isAssistantMessage
|
||||
const showMetadata = Boolean(message.usage) && !generating
|
||||
|
||||
const onCopy = useCallback(() => {
|
||||
navigator.clipboard.writeText(message.content)
|
||||
@@ -50,88 +55,89 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
}, [message.content, t])
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
const confirmed = await window.modal.confirm({
|
||||
icon: null,
|
||||
title: t('message.message.delete.title'),
|
||||
content: t('message.message.delete.content'),
|
||||
okText: t('common.delete'),
|
||||
okType: 'danger'
|
||||
})
|
||||
confirmed && onDeleteMessage?.(message)
|
||||
}, [message, onDeleteMessage, t])
|
||||
|
||||
const onEdit = useCallback(() => EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, message), [message])
|
||||
|
||||
const onRegenerate = useCallback(() => {
|
||||
onDeleteMessage?.(message)
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE), 100)
|
||||
}, [message, onDeleteMessage])
|
||||
const onRegenerate = useCallback(
|
||||
(model: Model) => {
|
||||
setModel(model)
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE, model), 100)
|
||||
},
|
||||
[setModel]
|
||||
)
|
||||
|
||||
const getUserName = useCallback(() => {
|
||||
if (message.id === 'assistant') {
|
||||
return assistant?.name
|
||||
}
|
||||
|
||||
if (message.role === 'assistant') {
|
||||
return upperFirst(message.modelId)
|
||||
}
|
||||
|
||||
if (message.id === 'assistant') return assistant?.name
|
||||
if (message.role === 'assistant') return upperFirst(message.modelId)
|
||||
return userName || t('common.you')
|
||||
}, [assistant?.name, message.id, message.modelId, message.role, t, userName])
|
||||
|
||||
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" : 'Poppins, -apple-system, sans-serif'
|
||||
const fontFamily = useMemo(() => {
|
||||
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
|
||||
}, [messageFont])
|
||||
|
||||
const messageBorder = showMessageDivider ? undefined : 'none'
|
||||
const avatarSource = useMemo(() => (message.modelId ? getModelLogo(message.modelId) : undefined), [message.modelId])
|
||||
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
|
||||
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<MessageContainer key={message.id} className="message" style={{ border: messageBorder }}>
|
||||
<MessageHeader>
|
||||
<AvatarWrapper>
|
||||
{message.role === 'assistant' ? (
|
||||
<Avatar src={message.modelId ? getModelLogo(message.modelId) : undefined} size={35}>
|
||||
{firstLetter(assistant?.name).toUpperCase()}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar src={avatar} size={35} />
|
||||
)}
|
||||
<UserWrap>
|
||||
<UserName>{removeLeadingEmoji(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} />
|
||||
</MessageContentLoading>
|
||||
)}
|
||||
{message.status !== 'sending' && <Markdown message={message} />}
|
||||
{message.usage && !generating && (
|
||||
<MessageMetadata>
|
||||
Tokens: {message.usage.total_tokens} | ↑{message.usage.prompt_tokens}↓{message.usage.completion_tokens}
|
||||
</MessageMetadata>
|
||||
const dropdownItems = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: t('chat.save'),
|
||||
key: 'save',
|
||||
icon: <SaveOutlined />,
|
||||
onClick: () => {
|
||||
const fileName = message.createdAt + '.md'
|
||||
window.api.saveFile(fileName, message.content)
|
||||
}
|
||||
}
|
||||
],
|
||||
[t, message]
|
||||
)
|
||||
|
||||
const MessageItem = useCallback(() => {
|
||||
if (message.status === 'sending') {
|
||||
return (
|
||||
<MessageContentLoading>
|
||||
<SyncOutlined spin size={24} />
|
||||
</MessageContentLoading>
|
||||
)
|
||||
}
|
||||
|
||||
if (message.status === 'error') {
|
||||
return (
|
||||
<Alert
|
||||
message={t('error.chat.response')}
|
||||
description={<Markdown message={message} />}
|
||||
type="error"
|
||||
style={{ marginBottom: 15 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <Markdown message={message} />
|
||||
}, [message])
|
||||
|
||||
return (
|
||||
<MessageContainer key={message.id} className="message">
|
||||
<MessageHeader>
|
||||
<AvatarWrapper>
|
||||
{isAssistantMessage ? (
|
||||
<Avatar src={avatarSource} size={35} style={{ borderRadius: '20%' }}>
|
||||
{avatarName}
|
||||
</Avatar>
|
||||
) : (
|
||||
<Avatar src={avatar} size={35} style={{ borderRadius: '20%' }} />
|
||||
)}
|
||||
<UserWrap>
|
||||
<UserName>{username}</UserName>
|
||||
<MessageTime>{dayjs(message.createdAt).format('MM/DD HH:mm')}</MessageTime>
|
||||
</UserWrap>
|
||||
</AvatarWrapper>
|
||||
</MessageHeader>
|
||||
<MessageContent style={{ fontFamily }}>
|
||||
<MessageItem />
|
||||
<MessageFooter style={{ border: messageBorder }}>
|
||||
{showMenu && (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
|
||||
{message.role === 'user' && (
|
||||
@@ -143,24 +149,32 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
)}
|
||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onCopy}>
|
||||
{!copied && <CopyOutlined />}
|
||||
{!copied && <i className="iconfont icon-copy"></i>}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onDelete}>
|
||||
<DeleteOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{canRegenerate && (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onRegenerate}>
|
||||
<SyncOutlined />
|
||||
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topRight">
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</SelectModelDropdown>
|
||||
)}
|
||||
<Popconfirm
|
||||
title={t('message.message.delete.content')}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onConfirm={() => onDeleteMessage?.(message)}>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
|
||||
<ActionButton>
|
||||
<DeleteOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Popconfirm>
|
||||
{!isUserMessage && (
|
||||
<Dropdown menu={{ items: getDropdownMenus(message) }} trigger={['click']} placement="topRight" arrow>
|
||||
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
|
||||
<ActionButton>
|
||||
<MenuOutlined />
|
||||
</ActionButton>
|
||||
@@ -168,38 +182,23 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
||||
)}
|
||||
</MenusBar>
|
||||
)}
|
||||
</MessageContent>
|
||||
</MessageContainer>
|
||||
),
|
||||
[
|
||||
assistant?.name,
|
||||
avatar,
|
||||
canRegenerate,
|
||||
copied,
|
||||
fontFamily,
|
||||
generating,
|
||||
getDropdownMenus,
|
||||
getUserName,
|
||||
isLastMessage,
|
||||
isUserMessage,
|
||||
message,
|
||||
messageBorder,
|
||||
onCopy,
|
||||
onDelete,
|
||||
onEdit,
|
||||
onRegenerate,
|
||||
showMenu,
|
||||
t
|
||||
]
|
||||
{showMetadata && (
|
||||
<MessageMetadata>
|
||||
Tokens: {message?.usage?.total_tokens} | ↑{message?.usage?.prompt_tokens} | ↓
|
||||
{message?.usage?.completion_tokens}
|
||||
</MessageMetadata>
|
||||
)}
|
||||
</MessageFooter>
|
||||
</MessageContent>
|
||||
</MessageContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 20px;
|
||||
padding: 0 20px;
|
||||
position: relative;
|
||||
border-bottom: 0.5px dotted var(--color-border);
|
||||
.menubar {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
@@ -208,7 +207,7 @@ const MessageContainer = styled.div`
|
||||
}
|
||||
&.user {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
@@ -260,6 +259,16 @@ const MessageContent = styled.div`
|
||||
margin-top: 5px;
|
||||
`
|
||||
|
||||
const MessageFooter = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2px 0;
|
||||
margin: 2px 0 8px 0;
|
||||
border-top: 0.5px dashed var(--color-border);
|
||||
`
|
||||
|
||||
const MessageContentLoading = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -273,17 +282,18 @@ const MenusBar = styled.div`
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-left: -5px;
|
||||
`
|
||||
|
||||
const MessageMetadata = styled.div`
|
||||
font-size: 12px;
|
||||
color: var(--color-text-2);
|
||||
user-select: text;
|
||||
margin: 2px 0;
|
||||
`
|
||||
|
||||
const ActionButton = styled.div`
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -291,7 +301,15 @@ const ActionButton = styled.div`
|
||||
align-items: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
.anticon {
|
||||
transition: all 0.3s ease;
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
.anticon {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
.anticon,
|
||||
.iconfont {
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: var(--color-icon);
|
||||
@@ -301,4 +319,4 @@ const ActionButton = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export default MessageItem
|
||||
export default memo(MessageItem)
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Assistant, Message, Topic } from '@renderer/types'
|
||||
import localforage from 'localforage'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import MessageItem from './Message'
|
||||
import { debounce, reverse } from 'lodash'
|
||||
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { estimateHistoryTokenCount, runAsyncFunction } from '@renderer/utils'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
|
||||
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { estimateHistoryTokenCount, filterMessages } from '@renderer/services/messages'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { Assistant, Message, Model, Topic } from '@renderer/types'
|
||||
import { getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils'
|
||||
import { t } from 'i18next'
|
||||
import localforage from 'localforage'
|
||||
import { last, reverse } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageItem from './Message'
|
||||
import Suggestions from './Suggestions'
|
||||
|
||||
interface Props {
|
||||
@@ -21,47 +23,47 @@ interface Props {
|
||||
const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
const [lastMessage, setLastMessage] = useState<Message | null>(null)
|
||||
const { updateTopic } = useAssistant(assistant.id)
|
||||
const provider = useProviderByAssistant(assistant)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const { updateTopic } = useAssistant(assistant.id)
|
||||
|
||||
const assistantDefaultMessage: Message = {
|
||||
id: 'assistant',
|
||||
role: 'assistant',
|
||||
content: assistant.description || assistant.prompt || t('assistant.default.description'),
|
||||
assistantId: assistant.id,
|
||||
topicId: topic.id,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString()
|
||||
}
|
||||
const assistantDefaultMessage: Message = useMemo(
|
||||
() => ({
|
||||
id: 'assistant',
|
||||
role: 'assistant',
|
||||
content: assistant.description || assistant.prompt || t('chat.default.description'),
|
||||
assistantId: assistant.id,
|
||||
topicId: topic.id,
|
||||
status: 'pending',
|
||||
createdAt: new Date().toISOString()
|
||||
}),
|
||||
[assistant.description, assistant.id, assistant.prompt, topic.id]
|
||||
)
|
||||
|
||||
const onSendMessage = useCallback(
|
||||
(message: Message) => {
|
||||
const _messages = [...messages, message]
|
||||
setMessages(_messages)
|
||||
localforage.setItem(`topic:${topic.id}`, {
|
||||
...topic,
|
||||
messages: _messages
|
||||
})
|
||||
localforage.setItem(`topic:${topic.id}`, { ...topic, messages: _messages })
|
||||
},
|
||||
[messages, topic]
|
||||
)
|
||||
|
||||
const autoRenameTopic = useCallback(async () => {
|
||||
if (topic.name === t('assistant.default.topic.name') && messages.length >= 2) {
|
||||
if (topic.name === t('chat.default.topic.name') && messages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||
summaryText && updateTopic({ ...topic, name: summaryText })
|
||||
}
|
||||
}, [assistant, messages, topic, updateTopic])
|
||||
|
||||
const onDeleteMessage = (message: Message) => {
|
||||
const _messages = messages.filter((m) => m.id !== message.id)
|
||||
setMessages(_messages)
|
||||
localforage.setItem(`topic:${topic.id}`, {
|
||||
id: topic.id,
|
||||
messages: _messages
|
||||
})
|
||||
}
|
||||
const onDeleteMessage = useCallback(
|
||||
(message: Message) => {
|
||||
const _messages = messages.filter((m) => m.id !== message.id)
|
||||
setMessages(_messages)
|
||||
localforage.setItem(`topic:${topic.id}`, { id: topic.id, messages: _messages })
|
||||
},
|
||||
[messages, topic.id]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [
|
||||
@@ -74,8 +76,18 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
onSendMessage(msg)
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async () => {
|
||||
fetchChatCompletion({ assistant, messages: messages, topic, onResponse: setLastMessage })
|
||||
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async (model: Model) => {
|
||||
const lastUserMessage = last(filterMessages(messages).filter((m) => m.role === 'user'))
|
||||
if (lastUserMessage) {
|
||||
const content = `[@${model.name}](#) ${getBriefInfo(lastUserMessage.content)}`
|
||||
onSendMessage({ ...lastUserMessage, id: uuid(), type: '@', content })
|
||||
fetchChatCompletion({
|
||||
assistant,
|
||||
topic,
|
||||
messages: [...messages, lastUserMessage],
|
||||
onResponse: setLastMessage
|
||||
})
|
||||
}
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic),
|
||||
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
|
||||
@@ -85,27 +97,18 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
||||
})
|
||||
]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [assistant, autoRenameTopic, messages, onSendMessage, provider, topic, updateTopic])
|
||||
}, [assistant, messages, provider, topic, autoRenameTopic, updateTopic, onSendMessage])
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
const messages = await LocalStorage.getTopicMessages(topic.id)
|
||||
setMessages(messages || [])
|
||||
const messages = (await LocalStorage.getTopicMessages(topic.id)) || []
|
||||
setMessages(messages)
|
||||
})
|
||||
}, [topic.id])
|
||||
|
||||
const scrollTop = useCallback(
|
||||
debounce(() => containerRef.current?.scrollTo({ top: 100000, behavior: 'auto' }), 500, {
|
||||
leading: true,
|
||||
trailing: false
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(scrollTop, 100)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [messages, lastMessage])
|
||||
setTimeout(() => containerRef.current?.scrollTo({ top: containerRef.current.scrollHeight, behavior: 'auto' }), 0)
|
||||
}, [messages])
|
||||
|
||||
useEffect(() => {
|
||||
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
|
||||
@@ -129,6 +132,7 @@ const Container = styled.div`
|
||||
overflow-y: auto;
|
||||
flex-direction: column-reverse;
|
||||
max-height: calc(100vh - var(--input-bar-height) - var(--navbar-height));
|
||||
padding: 10px 0;
|
||||
`
|
||||
|
||||
export default Messages
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import { CodeSandboxOutlined } from '@ant-design/icons'
|
||||
import { NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
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 { Avatar, Button, Dropdown, MenuProps } from 'antd'
|
||||
import { first, upperFirst } from 'lodash'
|
||||
import { removeLeadingEmoji } from '@renderer/utils'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { NewButton } from '../HomePage'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { removeLeadingEmoji } from '@renderer/utils'
|
||||
import SelectModelButton from './SelectModelButton'
|
||||
|
||||
interface Props {
|
||||
activeAssistant: Assistant
|
||||
@@ -20,30 +17,9 @@ interface Props {
|
||||
|
||||
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)
|
||||
.map((p) => ({
|
||||
key: p.id,
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
type: 'group',
|
||||
children: p.models.map((m) => ({
|
||||
key: m?.id,
|
||||
label: upperFirst(m?.name),
|
||||
style: m?.id === model?.id ? { color: 'var(--color-primary)' } : undefined,
|
||||
icon: (
|
||||
<Avatar src={getModelLogo(m?.id || '')} size={24}>
|
||||
{first(m?.name)}
|
||||
</Avatar>
|
||||
),
|
||||
onClick: () => m && setModel(m)
|
||||
}))
|
||||
}))
|
||||
|
||||
return (
|
||||
<NavbarCenter style={{ paddingLeft: isMac ? 16 : 8 }}>
|
||||
{!showAssistants && (
|
||||
@@ -51,39 +27,17 @@ const NavigationCenter: FC<Props> = ({ activeAssistant }) => {
|
||||
<i className="iconfont icon-showsidebarhoriz" />
|
||||
</NewButton>
|
||||
)}
|
||||
<AssistantName>{removeLeadingEmoji(assistant?.name) || t('assistant.default.name')}</AssistantName>
|
||||
<DropdownMenu
|
||||
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' } }}
|
||||
trigger={['click']}
|
||||
overlayClassName="chat-nav-dropdown">
|
||||
<DropdownButton size="small" type="primary" ghost>
|
||||
<CodeSandboxOutlined />
|
||||
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
|
||||
</DropdownButton>
|
||||
</DropdownMenu>
|
||||
<AssistantName>{removeLeadingEmoji(assistant?.name) || t('chat.default.name')}</AssistantName>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</NavbarCenter>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenu = styled(Dropdown)`
|
||||
-webkit-app-region: none;
|
||||
margin-left: 10px;
|
||||
`
|
||||
|
||||
const AssistantName = styled.span`
|
||||
font-weight: bold;
|
||||
margin-left: 5px;
|
||||
`
|
||||
|
||||
const DropdownButton = styled(Button)`
|
||||
font-size: 11px;
|
||||
border-radius: 15px;
|
||||
padding: 0 8px;
|
||||
`
|
||||
|
||||
const ModelName = styled.span`
|
||||
margin-left: -2px;
|
||||
font-weight: bolder;
|
||||
margin-right: 10px;
|
||||
font-family: Ubuntu;
|
||||
`
|
||||
|
||||
export default NavigationCenter
|
||||
|
||||
41
src/renderer/src/pages/home/components/SelectModelButton.tsx
Normal file
41
src/renderer/src/pages/home/components/SelectModelButton.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { Button } from 'antd'
|
||||
import { upperFirst } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SelectModelDropdown from './SelectModelDropdown'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
}
|
||||
|
||||
const SelectModelButton: FC<Props> = ({ assistant }) => {
|
||||
const { model, setModel } = useAssistant(assistant.id)
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<SelectModelDropdown model={model} onSelect={setModel}>
|
||||
<DropdownButton size="small" type="default">
|
||||
<ModelAvatar model={model} size={20} />
|
||||
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
|
||||
</DropdownButton>
|
||||
</SelectModelDropdown>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownButton = styled(Button)`
|
||||
font-size: 11px;
|
||||
border-radius: 15px;
|
||||
padding: 12px 8px 12px 3px;
|
||||
`
|
||||
|
||||
const ModelName = styled.span`
|
||||
margin-left: -2px;
|
||||
font-weight: bolder;
|
||||
`
|
||||
|
||||
export default SelectModelButton
|
||||
@@ -0,0 +1,55 @@
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd'
|
||||
import { first, sortBy, upperFirst } from 'lodash'
|
||||
import { FC, PropsWithChildren } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props extends DropdownProps {
|
||||
model: Model
|
||||
onSelect: (model: Model) => void
|
||||
}
|
||||
|
||||
const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, onSelect, ...props }) => {
|
||||
const { t } = useTranslation()
|
||||
const { providers } = useProviders()
|
||||
|
||||
const items: MenuProps['items'] = providers
|
||||
.filter((p) => p.models.length > 0)
|
||||
.map((p) => ({
|
||||
key: p.id,
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
type: 'group',
|
||||
children: sortBy(p.models, 'name').map((m) => ({
|
||||
key: m?.id,
|
||||
label: upperFirst(m?.name),
|
||||
defaultSelectedKeys: [model?.id],
|
||||
icon: (
|
||||
<Avatar src={getModelLogo(m?.id || '')} size={24}>
|
||||
{first(m?.name)}
|
||||
</Avatar>
|
||||
),
|
||||
onClick: () => m && onSelect(m)
|
||||
}))
|
||||
}))
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' }, selectedKeys: [model?.id] }}
|
||||
trigger={['click']}
|
||||
arrow
|
||||
placement="bottom"
|
||||
overlayClassName="chat-nav-dropdown"
|
||||
{...props}>
|
||||
{children}
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenu = styled(Dropdown)`
|
||||
-webkit-app-region: none;
|
||||
`
|
||||
|
||||
export default SelectModelDropdown
|
||||
@@ -1,45 +0,0 @@
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { ArrowUpOutlined, EnterOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SendOutlined } from '@ant-design/icons'
|
||||
|
||||
interface Props {
|
||||
sendMessage: () => void
|
||||
}
|
||||
|
||||
const SendMessageButton: FC<Props> = ({ sendMessage }) => {
|
||||
const { sendMessageShortcut, setSendMessageShortcut } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const sendSettingItems: MenuProps['items'] = [
|
||||
{
|
||||
label: `Enter ${t('assistant.input.send')}`,
|
||||
key: 'Enter',
|
||||
icon: <EnterOutlined />,
|
||||
onClick: () => setSendMessageShortcut('Enter')
|
||||
},
|
||||
{
|
||||
label: `Shift+Enter ${t('assistant.input.send')}`,
|
||||
key: 'Shift+Enter',
|
||||
icon: <ArrowUpOutlined />,
|
||||
onClick: () => setSendMessageShortcut('Shift+Enter')
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Dropdown.Button
|
||||
size="small"
|
||||
onClick={sendMessage}
|
||||
trigger={['click']}
|
||||
arrow
|
||||
menu={{ items: sendSettingItems, selectable: true, defaultSelectedKeys: [sendMessageShortcut] }}
|
||||
style={{ width: 'auto' }}>
|
||||
{t('assistant.input.send')}
|
||||
<SendOutlined />
|
||||
</Dropdown.Button>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendMessageButton
|
||||
@@ -1,11 +1,11 @@
|
||||
import { fetchSuggestions } from '@renderer/services/api'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Assistant, Message, Suggestion } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import dayjs from 'dayjs'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import BeatLoader from 'react-spinners/BeatLoader'
|
||||
import { fetchSuggestions } from '@renderer/services/api'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -86,7 +86,7 @@ const Suggestions: FC<Props> = ({ assistant, messages, lastMessage }) => {
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 10px 20px 55px;
|
||||
padding: 10px 10px 20px 65px;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { fetchMessagesSummary } from '@renderer/services/api'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { DeleteOutlined, EditOutlined, OpenAIOutlined } 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: <OpenAIOutlined />,
|
||||
async onClick() {
|
||||
const messages = await LocalStorage.getTopicMessages(topic.id)
|
||||
if (messages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||
if (summaryText) {
|
||||
updateTopic({ ...topic, name: summaryText })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('assistant.topics.edit.title'),
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const name = await PromptPopup.show({
|
||||
title: t('assistant.topics.edit.title'),
|
||||
message: '',
|
||||
defaultValue: topic?.name || ''
|
||||
})
|
||||
if (name && topic?.name !== name) {
|
||||
updateTopic({ ...topic, name })
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
if (assistant.topics.length > 1) {
|
||||
menus.push({ type: 'divider' })
|
||||
menus.push({
|
||||
label: t('common.delete'),
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick() {
|
||||
if (assistant.topics.length === 1) return
|
||||
removeTopic(topic)
|
||||
setActiveTopic(assistant.topics[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return menus
|
||||
}
|
||||
|
||||
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: 3px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: Poppins;
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
|
||||
export default TopicsTab
|
||||
@@ -5,24 +5,27 @@ import {
|
||||
FullscreenOutlined,
|
||||
HistoryOutlined,
|
||||
PauseCircleOutlined,
|
||||
PlusCircleOutlined
|
||||
PlusCircleOutlined,
|
||||
QuestionCircleOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { estimateInputTokenCount } from '@renderer/services/messages'
|
||||
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 { uuid } from '@renderer/utils'
|
||||
import { Button, Divider, Popconfirm, Tag, 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 SendMessageButton from './SendMessageButton'
|
||||
|
||||
interface Props {
|
||||
@@ -30,8 +33,11 @@ interface Props {
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
let _text = ''
|
||||
|
||||
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const [text, setText] = useState('')
|
||||
const [text, setText] = useState(_text)
|
||||
const [inputFocus, setInputFocus] = useState(false)
|
||||
const { addTopic } = useAssistant(assistant.id)
|
||||
const { sendMessageShortcut, showInputEstimatedTokens } = useSettings()
|
||||
const [expended, setExpend] = useState(false)
|
||||
@@ -40,7 +46,9 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const inputRef = useRef<TextAreaRef>(null)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const sendMessage = () => {
|
||||
_text = text
|
||||
|
||||
const sendMessage = useCallback(() => {
|
||||
if (generating) {
|
||||
return
|
||||
}
|
||||
@@ -64,21 +72,15 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
setText('')
|
||||
|
||||
setExpend(false)
|
||||
}
|
||||
}, [assistant.id, assistant.topics, generating, text])
|
||||
|
||||
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (expended) {
|
||||
if (event.key === 'Escape') {
|
||||
setExpend(false)
|
||||
return
|
||||
return setExpend(false)
|
||||
}
|
||||
if (event.key === 'Enter' && event.shiftKey) {
|
||||
sendMessage()
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (sendMessageShortcut === 'Enter' && event.key === 'Enter') {
|
||||
@@ -140,70 +142,88 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
}, [assistant])
|
||||
|
||||
return (
|
||||
<Container id="inputbar" style={{ minHeight: expended ? '100%' : 'var(--input-bar-height)' }}>
|
||||
<Toolbar onDoubleClick={() => setExpend(!expended)}>
|
||||
<Container
|
||||
id="inputbar"
|
||||
style={{ minHeight: expended ? '60%' : 'var(--input-bar-height)' }}
|
||||
className={inputFocus ? 'focus' : ''}>
|
||||
<Toolbar>
|
||||
<ToolbarMenu>
|
||||
<Tooltip placement="top" title={t('assistant.input.new_chat')} arrow>
|
||||
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
|
||||
<ToolbarButton type="text" onClick={addNewTopic}>
|
||||
<PlusCircleOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('assistant.input.clear')} arrow>
|
||||
<Tooltip placement="top" title={t('chat.input.topics')} arrow>
|
||||
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)}>
|
||||
<HistoryOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
|
||||
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS)}>
|
||||
<ControlOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
|
||||
<Popconfirm
|
||||
icon={false}
|
||||
title={t('assistant.input.clear.title')}
|
||||
description={t('assistant.input.clear.content')}
|
||||
title={t('chat.input.clear.content')}
|
||||
placement="top"
|
||||
onConfirm={clearTopic}
|
||||
okText={t('assistant.input.clear')}>
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
okText={t('chat.input.clear')}>
|
||||
<ToolbarButton type="text">
|
||||
<ClearOutlined />
|
||||
</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>
|
||||
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
|
||||
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
{showInputEstimatedTokens && (
|
||||
<TextCount>
|
||||
<HistoryOutlined /> {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} | T↑
|
||||
{`${inputTokenCount}/${estimateTokenCount}`}
|
||||
<Tooltip title={t('chat.input.context_count.tip') + ' | ' + t('chat.input.estimated_tokens.tip')}>
|
||||
<Tag
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
borderRadius: '20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '2px 8px'
|
||||
}}>
|
||||
<i className="iconfont icon-history" style={{ marginRight: '3px' }} />
|
||||
{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}
|
||||
<Divider type="vertical" style={{ marginTop: 2, marginLeft: 5, marginRight: 5 }} />↑
|
||||
{`${inputTokenCount} / ${estimateTokenCount}`}
|
||||
</Tag>
|
||||
</Tooltip>
|
||||
</TextCount>
|
||||
)}
|
||||
</ToolbarMenu>
|
||||
<ToolbarMenu>
|
||||
{generating && (
|
||||
<Tooltip placement="top" title={t('assistant.input.pause')} arrow>
|
||||
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
||||
<ToolbarButton type="text" onClick={onPause}>
|
||||
<PauseCircleOutlined style={{ color: 'var(--color-error)' }} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<SendMessageButton sendMessage={sendMessage} />
|
||||
<SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={t('assistant.input.placeholder')}
|
||||
placeholder={t('chat.input.placeholder')}
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
variant="borderless"
|
||||
ref={inputRef}
|
||||
styles={{ textarea: { paddingLeft: 0 } }}
|
||||
onFocus={() => setInputFocus(true)}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
@@ -212,10 +232,14 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
border-top: 0.5px solid var(--color-border);
|
||||
height: var(--input-bar-height);
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
margin: 0 20px 15px 20px;
|
||||
border-radius: 10px;
|
||||
&.focus {
|
||||
}
|
||||
`
|
||||
|
||||
const Textarea = styled(TextArea)`
|
||||
@@ -224,13 +248,16 @@ const Textarea = styled(TextArea)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
margin: 0 15px 5px 15px;
|
||||
font-family: Ubuntu;
|
||||
resize: vertical;
|
||||
overflow: auto;
|
||||
`
|
||||
|
||||
const Toolbar = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 3px 10px;
|
||||
padding: 0 10px;
|
||||
`
|
||||
|
||||
const ToolbarMenu = styled.div`
|
||||
@@ -0,0 +1,23 @@
|
||||
import { FC } from 'react'
|
||||
|
||||
interface Props {
|
||||
disabled: boolean
|
||||
sendMessage: () => void
|
||||
}
|
||||
|
||||
const SendMessageButton: FC<Props> = ({ disabled, sendMessage }) => {
|
||||
return (
|
||||
<i
|
||||
className="iconfont icon-ic_send"
|
||||
onClick={sendMessage}
|
||||
style={{
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)',
|
||||
fontSize: 22,
|
||||
transition: 'all 0.2s'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendMessageButton
|
||||
@@ -7,7 +7,8 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||
import { atomDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||
import styled from 'styled-components'
|
||||
import Mermaid from '../Mermaid'
|
||||
|
||||
import Mermaid from './Mermaid'
|
||||
|
||||
interface CodeBlockProps {
|
||||
children: string
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { omit } from 'lodash'
|
||||
import React from 'react'
|
||||
|
||||
const Link: React.FC = (props) => {
|
||||
const Link: React.FC = (props: React.AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
if (props.href?.startsWith('#')) {
|
||||
return <span className="link">{props.children}</span>
|
||||
}
|
||||
|
||||
return <a {...omit(props, 'node')} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()} />
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { FC, useCallback, useMemo } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
import { Message } from '@renderer/types'
|
||||
import { convertMathFormula } from '@renderer/utils'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CodeBlock from './CodeBlock'
|
||||
import Link from './Link'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import remarkMath from 'remark-math'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
import CodeBlock from './CodeBlock'
|
||||
import Link from './Link'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@@ -17,27 +20,29 @@ interface Props {
|
||||
const Markdown: FC<Props> = ({ message }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getMessageContent = useCallback(
|
||||
(message: Message) => {
|
||||
const empty = isEmpty(message.content)
|
||||
const paused = message.status === 'paused'
|
||||
return empty && paused ? t('message.chat.completion.paused') : message.content
|
||||
},
|
||||
[t]
|
||||
)
|
||||
const messageContent = useMemo(() => {
|
||||
const empty = isEmpty(message.content)
|
||||
const paused = message.status === 'paused'
|
||||
const content = empty && paused ? t('message.chat.completion.paused') : message.content
|
||||
return convertMathFormula(content)
|
||||
}, [message.content, message.status, t])
|
||||
|
||||
return useMemo(() => {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
className="markdown"
|
||||
remarkPlugins={[remarkGfm, remarkMath]}
|
||||
remarkRehypeOptions={{ footnoteLabel: t('common.footnote'), footnoteLabelTagName: 'h4' }}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
remarkPlugins={[[remarkMath, { singleDollarTextMath: false }], remarkGfm]}
|
||||
remarkRehypeOptions={{
|
||||
footnoteLabel: t('common.footnotes'),
|
||||
footnoteLabelTagName: 'h4',
|
||||
footnoteBackContent: ' '
|
||||
}}
|
||||
components={{ code: CodeBlock as any, a: Link as any }}>
|
||||
{getMessageContent(message)}
|
||||
{messageContent}
|
||||
</ReactMarkdown>
|
||||
)
|
||||
}, [getMessageContent, message, t])
|
||||
}, [messageContent, t])
|
||||
}
|
||||
|
||||
export default Markdown
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
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 { BarsOutlined, SettingOutlined } from '@ant-design/icons'
|
||||
import { useShowRightSidebar } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Segmented } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import SettingsTab from './SettingsTab'
|
||||
import TopicsTab from './TopicsTab'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -47,54 +50,47 @@ const RightSidebar: FC<Props> = (props) => {
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [hideRightSidebar, isSettingsTab, isTopicTab, rightSidebarShown, showRightSidebar])
|
||||
|
||||
if (!rightSidebarShown) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container style={{ display: rightSidebarShown ? 'block' : 'none' }}>
|
||||
<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>
|
||||
<Segmented
|
||||
value={tab}
|
||||
style={{ borderRadius: 0, padding: '10px', gap: 5, borderBottom: '0.5px solid var(--color-border)' }}
|
||||
options={[
|
||||
{ label: t('common.topics'), value: 'topic', icon: <BarsOutlined /> },
|
||||
{ label: t('settings.title'), value: 'settings', icon: <SettingOutlined /> }
|
||||
]}
|
||||
block
|
||||
onChange={(value) => setTab(value as 'topic' | 'settings')}
|
||||
/>
|
||||
<TabContent>
|
||||
{tab === 'topic' && <TopicsTab {...props} />}
|
||||
{tab === 'settings' && <SettingsTab assistant={props.assistant} />}
|
||||
</TabContent>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--topic-list-width);
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
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;
|
||||
const TabContent = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
&.active {
|
||||
color: var(--color-text-2);
|
||||
font-weight: 600;
|
||||
}
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
`
|
||||
|
||||
export default RightSidebar
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Assistant } from '@renderer/types'
|
||||
import styled from 'styled-components'
|
||||
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { CheckOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { Col, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings/components'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setMessageFont, setShowInputEstimatedTokens, setShowMessageDivider } from '@renderer/store/settings'
|
||||
import { Assistant, AssistantSettings } from '@renderer/types'
|
||||
import { Col, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { debounce } from 'lodash'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { SettingDivider, SettingRow, SettingRowTitle, SettingSubtitle } from '@renderer/pages/settings/components'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setMessageFont, setShowInputEstimatedTokens, setShowMessageDivider } from '@renderer/store/settings'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
@@ -20,41 +21,49 @@ 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 [enableMaxTokens, setEnableMaxTokens] = useState(assistant?.settings?.enableMaxTokens ?? false)
|
||||
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { showMessageDivider, messageFont, showInputEstimatedTokens } = useSettings()
|
||||
const { showMessageDivider, messageFont, showInputEstimatedTokens, sendMessageShortcut, setSendMessageShortcut } =
|
||||
useSettings()
|
||||
|
||||
const onUpdateAssistantSettings = useCallback(
|
||||
debounce(
|
||||
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
|
||||
(settings: Partial<AssistantSettings>) => {
|
||||
updateAssistantSettings({
|
||||
...assistant.settings,
|
||||
temperature: _temperature ?? temperature,
|
||||
contextCount: _contextCount ?? contextCount
|
||||
temperature: settings.temperature ?? temperature,
|
||||
contextCount: settings.contextCount ?? contextCount,
|
||||
enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens,
|
||||
maxTokens: settings.maxTokens ?? maxTokens
|
||||
})
|
||||
},
|
||||
1000,
|
||||
{
|
||||
leading: false,
|
||||
trailing: true
|
||||
}
|
||||
{ leading: true, trailing: false }
|
||||
),
|
||||
[]
|
||||
[temperature, contextCount, enableMaxTokens, maxTokens]
|
||||
)
|
||||
|
||||
const onTemperatureChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
setTemperature(value)
|
||||
onUpdateAssistantSettings({ _temperature: value })
|
||||
onUpdateAssistantSettings({ temperature: value })
|
||||
}
|
||||
}
|
||||
|
||||
const onConextCountChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
setConextCount(value)
|
||||
onUpdateAssistantSettings({ _contextCount: value })
|
||||
onUpdateAssistantSettings({ contextCount: value })
|
||||
}
|
||||
}
|
||||
|
||||
const onMaxTokensChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
setMaxTokens(value)
|
||||
onUpdateAssistantSettings({ maxTokens: value })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +75,9 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
settings: {
|
||||
...assistant.settings,
|
||||
temperature: DEFAULT_TEMPERATURE,
|
||||
contextCount: DEFAULT_CONEXTCOUNT
|
||||
contextCount: DEFAULT_CONEXTCOUNT,
|
||||
enableMaxTokens: false,
|
||||
maxTokens: DEFAULT_MAX_TOKENS
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -74,73 +85,82 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
useEffect(() => {
|
||||
setTemperature(assistant?.settings?.temperature ?? DEFAULT_TEMPERATURE)
|
||||
setConextCount(assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
|
||||
setEnableMaxTokens(assistant?.settings?.enableMaxTokens ?? false)
|
||||
setMaxTokens(assistant?.settings?.maxTokens ?? DEFAULT_MAX_TOKENS)
|
||||
}, [assistant])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<SettingSubtitle>
|
||||
{t('settings.messages.model.title')}{' '}
|
||||
<Tooltip title={t('assistant.settings.reset')}>
|
||||
<ReloadOutlined onClick={onReset} style={{ cursor: 'pointer', fontSize: 13, padding: '0 3px' }} />
|
||||
<Tooltip title={t('chat.settings.reset')}>
|
||||
<ReloadOutlined onClick={onReset} style={{ cursor: 'pointer', fontSize: 12, padding: '0 3px' }} />
|
||||
</Tooltip>
|
||||
</SettingSubtitle>
|
||||
<SettingDivider />
|
||||
<Row align="middle">
|
||||
<Label>{t('assistant.settings.temperature')}</Label>
|
||||
<Tooltip title={t('assistant.settings.temperature.tip')}>
|
||||
<Label>{t('chat.settings.temperature')}</Label>
|
||||
<Tooltip title={t('chat.settings.temperature.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row align="middle" gutter={10}>
|
||||
<Col span={18}>
|
||||
<Col span={24}>
|
||||
<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')}>
|
||||
<Label>{t('chat.settings.conext_count')}</Label>
|
||||
<Tooltip title={t('chat.settings.conext_count.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row align="middle" gutter={10}>
|
||||
<Col span={18}>
|
||||
<Col span={24}>
|
||||
<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>
|
||||
<Row align="middle" justify="space-between" style={{ marginBottom: 8 }}>
|
||||
<HStack alignItems="center">
|
||||
<Label>{t('chat.settings.max_tokens')}</Label>
|
||||
<Tooltip title={t('chat.settings.max_tokens.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={enableMaxTokens}
|
||||
onChange={(enabled) => {
|
||||
setEnableMaxTokens(enabled)
|
||||
onUpdateAssistantSettings({ enableMaxTokens: enabled })
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
{enableMaxTokens && (
|
||||
<Row align="middle" gutter={10}>
|
||||
<Col span={24}>
|
||||
<Slider
|
||||
min={0}
|
||||
max={32000}
|
||||
onChange={onMaxTokensChange}
|
||||
value={typeof maxTokens === 'number' ? maxTokens : 0}
|
||||
step={100}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
<SettingSubtitle>{t('settings.messages.title')}</SettingSubtitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
@@ -172,28 +192,34 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
|
||||
</SettingRow>
|
||||
<Select
|
||||
value={sendMessageShortcut}
|
||||
menuItemSelectedIcon={<CheckOutlined />}
|
||||
options={[
|
||||
{ value: 'Enter', label: `Enter ${t('chat.input.send')}` },
|
||||
{ value: 'Shift+Enter', label: `Shift + Enter ${t('chat.input.send')}` }
|
||||
]}
|
||||
onChange={(value) => setSendMessageShortcut(value)}
|
||||
style={{ width: '100%', marginTop: 10 }}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
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;
|
||||
font-weight: 600;
|
||||
margin-right: 8px;
|
||||
`
|
||||
|
||||
160
src/renderer/src/pages/home/components/sidebar/TopicsTab.tsx
Normal file
160
src/renderer/src/pages/home/components/sidebar/TopicsTab.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { DeleteOutlined, EditOutlined, OpenAIOutlined } from '@ant-design/icons'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { fetchMessagesSummary } from '@renderer/services/api'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { droppableReorder } from '@renderer/utils'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import { FC, useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
activeTopic: Topic
|
||||
setActiveTopic: (topic: Topic) => void
|
||||
}
|
||||
|
||||
const TopicsTab: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
|
||||
const { assistant, removeTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
|
||||
const { t } = useTranslation()
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
|
||||
const getTopicMenuItems = useCallback(
|
||||
(topic: Topic) => {
|
||||
const menus: MenuProps['items'] = [
|
||||
{
|
||||
label: t('chat.topics.auto_rename'),
|
||||
key: 'auto-rename',
|
||||
icon: <OpenAIOutlined />,
|
||||
async onClick() {
|
||||
const messages = await LocalStorage.getTopicMessages(topic.id)
|
||||
if (messages.length >= 2) {
|
||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||
if (summaryText) {
|
||||
updateTopic({ ...topic, name: summaryText })
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.edit.title'),
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
async onClick() {
|
||||
const name = await PromptPopup.show({
|
||||
title: t('chat.topics.edit.title'),
|
||||
message: '',
|
||||
defaultValue: topic?.name || ''
|
||||
})
|
||||
if (name && topic?.name !== name) {
|
||||
updateTopic({ ...topic, name })
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
if (assistant.topics.length > 1) {
|
||||
menus.push({ type: 'divider' })
|
||||
menus.push({
|
||||
label: t('common.delete'),
|
||||
danger: true,
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
onClick() {
|
||||
if (assistant.topics.length === 1) return
|
||||
removeTopic(topic)
|
||||
setActiveTopic(assistant.topics[0])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return menus
|
||||
},
|
||||
[assistant, removeTopic, setActiveTopic, t, updateTopic]
|
||||
)
|
||||
|
||||
const onDragEnd = useCallback(
|
||||
(result: DropResult) => {
|
||||
if (result.destination) {
|
||||
const sourceIndex = result.source.index
|
||||
const destIndex = result.destination.index
|
||||
updateTopics(droppableReorder(assistant.topics, sourceIndex, destIndex))
|
||||
}
|
||||
},
|
||||
[assistant.topics, updateTopics]
|
||||
)
|
||||
|
||||
const onSwitchTopic = useCallback(
|
||||
(topic: Topic) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
setActiveTopic(topic)
|
||||
},
|
||||
[generating, setActiveTopic, t]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="droppable">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{assistant.topics.map((topic, index) => (
|
||||
<Draggable key={`draggable_${topic.id}_${index}`} draggableId={topic.id} index={index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
|
||||
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
|
||||
<TopicListItem
|
||||
className={topic.id === activeTopic?.id ? 'active' : ''}
|
||||
onClick={() => onSwitchTopic(topic)}>
|
||||
{topic.name}
|
||||
</TopicListItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 10px 10px;
|
||||
`
|
||||
|
||||
const TopicListItem = styled.div`
|
||||
padding: 7px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-family: Ubuntu;
|
||||
transition: all 0.3s;
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
|
||||
export default TopicsTab
|
||||
@@ -1,11 +1,15 @@
|
||||
import { Avatar, Button, Progress, Row, Tag } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { GithubOutlined } from '@ant-design/icons'
|
||||
import Logo from '@renderer/assets/images/logo.png'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { debounce } from 'lodash'
|
||||
import { Avatar, Button, Progress, Row, Tag } from 'antd'
|
||||
import { ProgressInfo } from 'electron-updater'
|
||||
import { debounce } from 'lodash'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
|
||||
|
||||
const AboutSettings: FC = () => {
|
||||
@@ -19,7 +23,13 @@ const AboutSettings: FC = () => {
|
||||
async () => {
|
||||
if (checkUpdateLoading || downloading) return
|
||||
setCheckUpdateLoading(true)
|
||||
await window.api.checkForUpdate()
|
||||
|
||||
try {
|
||||
await window.api.checkForUpdate()
|
||||
} catch (error) {
|
||||
setCheckUpdateLoading(false)
|
||||
}
|
||||
|
||||
setCheckUpdateLoading(false)
|
||||
},
|
||||
2000,
|
||||
@@ -83,7 +93,14 @@ const AboutSettings: FC = () => {
|
||||
|
||||
return (
|
||||
<SettingContainer>
|
||||
<SettingTitle>{t('settings.about.title')}</SettingTitle>
|
||||
<SettingTitle>
|
||||
{t('settings.about.title')}
|
||||
<HStack alignItems="center">
|
||||
<Link to="https://github.com/kangfenmao/cherry-studio">
|
||||
<GithubOutlined style={{ marginRight: 4, color: 'var(--color-text)', fontSize: 20 }} />
|
||||
</Link>
|
||||
</HStack>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<AboutHeader>
|
||||
<Row align="middle">
|
||||
|
||||
@@ -1,62 +1,80 @@
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { DEFAULT_CONEXTCOUNT, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { DEFAULT_CONEXTCOUNT, DEFAULT_MAX_TOKENS, DEFAULT_TEMPERATURE } from '@renderer/config/constant'
|
||||
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { Button, Col, Input, InputNumber, Row, Slider, Tooltip } from 'antd'
|
||||
import { AssistantSettings as AssistantSettingsType } from '@renderer/types'
|
||||
import { Button, Col, Input, InputNumber, Row, Slider, Switch, Tooltip } from 'antd'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
import { debounce } from 'lodash'
|
||||
import { FC, useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingSubtitle, SettingTitle } from './components'
|
||||
import { debounce } from 'lodash'
|
||||
|
||||
const AssistantSettings: FC = () => {
|
||||
const { defaultAssistant, updateDefaultAssistant } = useDefaultAssistant()
|
||||
const [temperature, setTemperature] = useState(defaultAssistant.settings?.temperature ?? DEFAULT_TEMPERATURE)
|
||||
const [contextCount, setConextCount] = useState(defaultAssistant.settings?.contextCount ?? DEFAULT_CONEXTCOUNT)
|
||||
const [enableMaxTokens, setEnableMaxTokens] = useState(defaultAssistant?.settings?.enableMaxTokens ?? false)
|
||||
const [maxTokens, setMaxTokens] = useState(defaultAssistant?.settings?.maxTokens ?? 0)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onUpdateAssistantSettings = useCallback(
|
||||
debounce(
|
||||
({ _temperature, _contextCount }: { _temperature?: number; _contextCount?: number }) => {
|
||||
(settings: Partial<AssistantSettingsType>) => {
|
||||
updateDefaultAssistant({
|
||||
...defaultAssistant,
|
||||
settings: {
|
||||
...defaultAssistant.settings,
|
||||
temperature: _temperature ?? temperature,
|
||||
contextCount: _contextCount ?? contextCount
|
||||
temperature: settings.temperature ?? temperature,
|
||||
contextCount: settings.contextCount ?? contextCount,
|
||||
enableMaxTokens: settings.enableMaxTokens ?? enableMaxTokens,
|
||||
maxTokens: settings.maxTokens ?? maxTokens
|
||||
}
|
||||
})
|
||||
},
|
||||
1000,
|
||||
{ leading: false, trailing: true }
|
||||
),
|
||||
[]
|
||||
[temperature, contextCount, enableMaxTokens, maxTokens]
|
||||
)
|
||||
|
||||
const onTemperatureChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
setTemperature(value)
|
||||
onUpdateAssistantSettings({ _temperature: value })
|
||||
onUpdateAssistantSettings({ temperature: value })
|
||||
}
|
||||
}
|
||||
|
||||
const onConextCountChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
setConextCount(value)
|
||||
onUpdateAssistantSettings({ _contextCount: value })
|
||||
onUpdateAssistantSettings({ contextCount: value })
|
||||
}
|
||||
}
|
||||
|
||||
const onMaxTokensChange = (value) => {
|
||||
if (!isNaN(value as number)) {
|
||||
setMaxTokens(value)
|
||||
onUpdateAssistantSettings({ maxTokens: value })
|
||||
}
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
setTemperature(DEFAULT_TEMPERATURE)
|
||||
setConextCount(DEFAULT_CONEXTCOUNT)
|
||||
setEnableMaxTokens(false)
|
||||
setMaxTokens(0)
|
||||
updateDefaultAssistant({
|
||||
...defaultAssistant,
|
||||
settings: {
|
||||
...defaultAssistant.settings,
|
||||
temperature: DEFAULT_TEMPERATURE,
|
||||
contextCount: DEFAULT_CONEXTCOUNT
|
||||
contextCount: DEFAULT_CONEXTCOUNT,
|
||||
enableMaxTokens: false,
|
||||
maxTokens: DEFAULT_MAX_TOKENS
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -79,15 +97,27 @@ const AssistantSettings: FC = () => {
|
||||
onChange={(e) => updateDefaultAssistant({ ...defaultAssistant, prompt: e.target.value })}
|
||||
/>
|
||||
<SettingDivider />
|
||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.assistant.model_params')}</SettingSubtitle>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
marginTop: 0,
|
||||
marginBottom: 20,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between'
|
||||
}}>
|
||||
<span>{t('settings.assistant.model_params')}</span>
|
||||
<Button onClick={onReset} style={{ width: 90 }}>
|
||||
{t('chat.settings.reset')}
|
||||
</Button>
|
||||
</SettingSubtitle>
|
||||
<Row align="middle">
|
||||
<Label>{t('assistant.settings.temperature')}</Label>
|
||||
<Tooltip title={t('assistant.settings.temperature.tip')}>
|
||||
<Label>{t('chat.settings.temperature')}</Label>
|
||||
<Tooltip title={t('chat.settings.temperature.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
|
||||
<Col span={22}>
|
||||
<Col span={21}>
|
||||
<Slider
|
||||
min={0}
|
||||
max={1.2}
|
||||
@@ -97,7 +127,7 @@ const AssistantSettings: FC = () => {
|
||||
step={0.1}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Col span={3}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={1.2}
|
||||
@@ -109,23 +139,23 @@ const AssistantSettings: FC = () => {
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="middle">
|
||||
<Label>{t('assistant.settings.conext_count')}</Label>
|
||||
<Tooltip title={t('assistant.settings.conext_count.tip')}>
|
||||
<Label>{t('chat.settings.conext_count')}</Label>
|
||||
<Tooltip title={t('chat.settings.conext_count.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</Row>
|
||||
<Row align="middle" style={{ marginBottom: 10 }} gutter={20}>
|
||||
<Col span={22}>
|
||||
<Col span={21}>
|
||||
<Slider
|
||||
min={0}
|
||||
max={20}
|
||||
marks={{ 0: '0', 5: '5', 10: '10', 15: '15', 20: t('assistant.settings.max') }}
|
||||
marks={{ 0: '0', 5: '5', 10: '10', 15: '15', 20: t('chat.settings.max') }}
|
||||
onChange={onConextCountChange}
|
||||
value={typeof contextCount === 'number' ? contextCount : 0}
|
||||
step={1}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
<Col span={3}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={20}
|
||||
@@ -136,9 +166,50 @@ const AssistantSettings: FC = () => {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Button onClick={onReset} style={{ width: 100 }}>
|
||||
{t('assistant.settings.reset')}
|
||||
</Button>
|
||||
<Row align="middle">
|
||||
<HStack alignItems="center">
|
||||
<Label>{t('chat.settings.max_tokens')}</Label>
|
||||
<Tooltip title={t('chat.settings.max_tokens.tip')}>
|
||||
<QuestionIcon />
|
||||
</Tooltip>
|
||||
</HStack>
|
||||
<Switch
|
||||
style={{ marginLeft: 10 }}
|
||||
checked={enableMaxTokens}
|
||||
onChange={(enabled) => {
|
||||
setEnableMaxTokens(enabled)
|
||||
onUpdateAssistantSettings({ enableMaxTokens: enabled })
|
||||
}}
|
||||
/>
|
||||
</Row>
|
||||
{enableMaxTokens && (
|
||||
<Row align="middle" gutter={20}>
|
||||
<Col span={21}>
|
||||
<Slider
|
||||
min={0}
|
||||
max={32000}
|
||||
onChange={onMaxTokensChange}
|
||||
value={typeof maxTokens === 'number' ? maxTokens : 0}
|
||||
step={100}
|
||||
marks={{
|
||||
0: '0',
|
||||
32000: t('chat.settings.max')
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={3}>
|
||||
<InputNumber
|
||||
min={0}
|
||||
max={32000}
|
||||
step={100}
|
||||
value={maxTokens}
|
||||
onChange={onMaxTokensChange}
|
||||
controls={true}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
)}
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
|
||||
import { Avatar, Input, Select, Upload } from 'antd'
|
||||
import styled from 'styled-components'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { compressImage, isValidProxyUrl } from '@renderer/utils'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import LocalStorage from '@renderer/services/storage'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar } from '@renderer/store/runtime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { setLanguage, setUserName, ThemeMode } from '@renderer/store/settings'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { compressImage, isValidProxyUrl } from '@renderer/utils'
|
||||
import { Avatar, Input, Select, Upload } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from './components'
|
||||
|
||||
const GeneralSettings: FC = () => {
|
||||
const avatar = useAvatar()
|
||||
@@ -22,8 +23,8 @@ const GeneralSettings: FC = () => {
|
||||
|
||||
const onSelectLanguage = (value: string) => {
|
||||
dispatch(setLanguage(value))
|
||||
i18n.changeLanguage(value)
|
||||
localStorage.setItem('language', value)
|
||||
i18n.changeLanguage(value)
|
||||
}
|
||||
|
||||
const onSetProxyUrl = () => {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { FC } from 'react'
|
||||
import { SettingContainer, SettingDivider, SettingTitle } from './components'
|
||||
import { Select } from 'antd'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { find } from 'lodash'
|
||||
import { Model } from '@renderer/types'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { EditOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Select } from 'antd'
|
||||
import { find, sortBy, upperFirst } from 'lodash'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingTitle } from './components'
|
||||
|
||||
const ModelSettings: FC = () => {
|
||||
const { defaultModel, topicNamingModel, translateModel, setDefaultModel, setTopicNamingModel, setTranslateModel } =
|
||||
@@ -20,8 +21,8 @@ const ModelSettings: FC = () => {
|
||||
.map((p) => ({
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
title: p.name,
|
||||
options: p.models.map((m) => ({
|
||||
label: m.name,
|
||||
options: sortBy(p.models, 'name').map((m) => ({
|
||||
label: upperFirst(m.name),
|
||||
value: m.id
|
||||
}))
|
||||
}))
|
||||
@@ -39,7 +40,7 @@ const ModelSettings: FC = () => {
|
||||
<SettingDivider />
|
||||
<Select
|
||||
defaultValue={defaultModel.id}
|
||||
style={{ width: 200 }}
|
||||
style={{ width: 360 }}
|
||||
onChange={(id) => setDefaultModel(find(allModels, { id }) as Model)}
|
||||
options={selectOptions}
|
||||
/>
|
||||
@@ -53,7 +54,7 @@ const ModelSettings: FC = () => {
|
||||
<SettingDivider />
|
||||
<Select
|
||||
defaultValue={topicNamingModel.id}
|
||||
style={{ width: 200 }}
|
||||
style={{ width: 360 }}
|
||||
onChange={(id) => setTopicNamingModel(find(allModels, { id }) as Model)}
|
||||
options={selectOptions}
|
||||
/>
|
||||
@@ -67,7 +68,7 @@ const ModelSettings: FC = () => {
|
||||
<SettingDivider />
|
||||
<Select
|
||||
defaultValue={translateModel?.id}
|
||||
style={{ width: 200 }}
|
||||
style={{ width: 360 }}
|
||||
onChange={(id) => setTranslateModel(find(allModels, { id }) as Model)}
|
||||
options={selectOptions}
|
||||
placeholder={t('settings.models.empty')}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getProviderLogo } from '@renderer/config/provider'
|
||||
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { droppableReorder, generateColorFromChar, getFirstCharacter, uuid } from '@renderer/utils'
|
||||
import { Avatar, Button, Dropdown, MenuProps, Tag } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import ProviderSetting from './components/ProviderSetting'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddProviderPopup from './components/AddProviderPopup'
|
||||
import ProviderSetting from './components/ProviderSetting'
|
||||
|
||||
const ProviderSettings: FC = () => {
|
||||
const providers = useAllProviders()
|
||||
@@ -92,7 +93,11 @@ const ProviderSettings: FC = () => {
|
||||
{providers.map((provider, index) => (
|
||||
<Draggable key={`draggable_${provider.id}_${index}`} draggableId={provider.id} index={index}>
|
||||
{(provided) => (
|
||||
<div ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
|
||||
<Dropdown
|
||||
menu={{ items: provider.isSystem ? [] : getDropdownMenus(provider) }}
|
||||
trigger={['contextMenu']}>
|
||||
@@ -166,7 +171,6 @@ const ProviderListItem = styled.div`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 5px 8px;
|
||||
margin-bottom: 5px;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
@@ -188,6 +192,7 @@ const ProviderItemName = styled.div`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
font-family: Ubuntu;
|
||||
`
|
||||
|
||||
const AddButtonWrapper = styled.div`
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, Route, Routes, useLocation } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
import GeneralSettings from './GeneralSettings'
|
||||
|
||||
import AboutSettings from './AboutSettings'
|
||||
import AssistantSettings from './AssistantSettings'
|
||||
import GeneralSettings from './GeneralSettings'
|
||||
import ModelSettings from './ModelSettings'
|
||||
import ProviderSettings from './ProviderSettings'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const SettingsPage: FC = () => {
|
||||
const { pathname } = useLocation()
|
||||
|
||||
@@ -115,18 +115,19 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
|
||||
export default class AddModelPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(this.topviewId)
|
||||
TopView.hide('AddModelPopup')
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
this.topviewId = TopView.show(
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
'AddModelPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -54,18 +54,19 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
||||
export default class AddProviderPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(this.topviewId)
|
||||
TopView.hide('AddProviderPopup')
|
||||
}
|
||||
static show(provider?: Provider) {
|
||||
return new Promise<string>((resolve) => {
|
||||
this.topviewId = TopView.show(
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
provider={provider}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
'AddProviderPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { LoadingOutlined, MinusOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import { SYSTEM_MODELS } from '@renderer/config/models'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { fetchModels } from '@renderer/services/api'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { Model, Provider } from '@renderer/types'
|
||||
import { getDefaultGroupName, isFreeModel, runAsyncFunction } from '@renderer/utils'
|
||||
import { Avatar, Button, Empty, Flex, Modal, Tag } from 'antd'
|
||||
import Search from 'antd/es/input/Search'
|
||||
import { groupBy, isEmpty, uniqBy } from 'lodash'
|
||||
import { useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { TopView } from '../../../components/TopView'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { TopView } from '../../../components/TopView'
|
||||
|
||||
interface ShowParams {
|
||||
provider: Provider
|
||||
@@ -223,18 +224,19 @@ const Question = styled(QuestionCircleOutlined)`
|
||||
export default class EditModelsPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide(this.topviewId)
|
||||
TopView.hide('EditModelsPopup')
|
||||
}
|
||||
static show(props: ShowParams) {
|
||||
return new Promise<any>((resolve) => {
|
||||
this.topviewId = TopView.show(
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
{...props}
|
||||
resolve={(v) => {
|
||||
resolve(v)
|
||||
this.hide()
|
||||
}}
|
||||
/>
|
||||
/>,
|
||||
'EditModelsPopup'
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
import { Provider } from '@renderer/types'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Avatar, Button, Card, Divider, Flex, Input, Space, Switch } from 'antd'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { groupBy } from 'lodash'
|
||||
import { SettingContainer, SettingSubtitle, SettingTitle } from '.'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import {
|
||||
CheckOutlined,
|
||||
EditOutlined,
|
||||
@@ -14,13 +6,23 @@ import {
|
||||
MinusCircleOutlined,
|
||||
PlusOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { getModelLogo } from '@renderer/config/provider'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
import { checkApi } from '@renderer/services/api'
|
||||
import { Provider } from '@renderer/types'
|
||||
import { Avatar, Button, Card, Divider, Flex, Input, Space, Switch } from 'antd'
|
||||
import Link from 'antd/es/typography/Link'
|
||||
import { groupBy } from 'lodash'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import OllamSettings from '../providers/OllamaSettings'
|
||||
import { SettingContainer, SettingSubtitle, SettingTitle } from '.'
|
||||
import AddModelPopup from './AddModelPopup'
|
||||
import EditModelsPopup from './EditModelsPopup'
|
||||
import Link from 'antd/es/typography/Link'
|
||||
import { checkApi } from '@renderer/services/api'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
||||
|
||||
interface Props {
|
||||
provider: Provider
|
||||
@@ -125,6 +127,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
||||
/>
|
||||
{apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>}
|
||||
</Space.Compact>
|
||||
{provider.id === 'ollama' && <OllamSettings />}
|
||||
<SettingSubtitle>{t('common.models')}</SettingSubtitle>
|
||||
{Object.keys(modelGroups).map((group) => (
|
||||
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
|
||||
@@ -181,14 +184,14 @@ const ModelListHeader = styled.div`
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const HelpTextRow = styled.div`
|
||||
export const HelpTextRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
`
|
||||
|
||||
const HelpText = styled.div`
|
||||
export const HelpText = styled.div`
|
||||
font-size: 11px;
|
||||
color: var(--color-text);
|
||||
opacity: 0.4;
|
||||
@@ -200,14 +203,11 @@ const HelpLink = styled(Link)`
|
||||
`
|
||||
|
||||
const RemoveIcon = styled(MinusCircleOutlined)`
|
||||
font-size: 18px;
|
||||
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
|
||||
|
||||
@@ -8,6 +8,7 @@ export const SettingContainer = styled.div`
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 15px;
|
||||
overflow-y: scroll;
|
||||
font-family: Ubuntu;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
@@ -26,7 +27,7 @@ export const SettingTitle = styled.div`
|
||||
export const SettingSubtitle = styled.div`
|
||||
font-size: 14px;
|
||||
color: var(--color-text-2);
|
||||
margin: 15px 0 10px 0;
|
||||
margin: 15px 0 0 0;
|
||||
user-select: none;
|
||||
`
|
||||
|
||||
|
||||
35
src/renderer/src/pages/settings/providers/OllamaSettings.tsx
Normal file
35
src/renderer/src/pages/settings/providers/OllamaSettings.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useOllamaSettings } from '@renderer/hooks/useOllama'
|
||||
import { InputNumber } from 'antd'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingSubtitle } from '../components'
|
||||
import { HelpText, HelpTextRow } from '../components/ProviderSetting'
|
||||
|
||||
const OllamSettings: FC = () => {
|
||||
const { keepAliveTime, setKeepAliveTime } = useOllamaSettings()
|
||||
const [keepAliveMinutes, setKeepAliveMinutes] = useState(keepAliveTime)
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<SettingSubtitle>{t('ollama.keep_alive_time.title')}</SettingSubtitle>
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
value={keepAliveMinutes}
|
||||
onChange={(e) => setKeepAliveMinutes(Number(e))}
|
||||
onBlur={() => setKeepAliveTime(keepAliveMinutes)}
|
||||
suffix={t('ollama.keep_alive_time.placeholder')}
|
||||
step={5}
|
||||
/>
|
||||
<HelpTextRow>
|
||||
<HelpText>{t('ollama.keep_alive_time.description')}</HelpText>
|
||||
</HelpTextRow>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div``
|
||||
|
||||
export default OllamSettings
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user