Compare commits
134 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9374541993 | ||
|
|
372224469d | ||
|
|
60e87e8a22 | ||
|
|
353e497642 | ||
|
|
0ee72a9ef8 | ||
|
|
d9873b4261 | ||
|
|
934ab1a374 | ||
|
|
33ac0937df | ||
|
|
f1c8922752 | ||
|
|
03bdbdb412 | ||
|
|
cf9d4c5370 | ||
|
|
bfa6bfa196 | ||
|
|
af8144d45e | ||
|
|
29605fbcdb | ||
|
|
6e7e5cb1f1 | ||
|
|
6f5dccd595 | ||
|
|
0af35b9f10 | ||
|
|
8350ac037e | ||
|
|
74b80b474e | ||
|
|
be4bf5b510 | ||
|
|
fdb610736d | ||
|
|
82e9baf211 | ||
|
|
e34d4be6f2 | ||
|
|
e7f7f8509e | ||
|
|
fa1f00f4f5 | ||
|
|
cee373bb6f | ||
|
|
01acdeb777 | ||
|
|
a654ccc25e | ||
|
|
71a35ccd44 | ||
|
|
29826ff091 | ||
|
|
8566476d91 | ||
|
|
a173a87f29 | ||
|
|
cb068d71ca | ||
|
|
66210d1d2e | ||
|
|
aa427c9911 | ||
|
|
9ae9fdf392 | ||
|
|
0ddef31ed8 | ||
|
|
617af8b12a | ||
|
|
71876e6a70 | ||
|
|
4f250cdcb1 | ||
|
|
9268ab845e | ||
|
|
0337c6649b | ||
|
|
8781388760 | ||
|
|
2016ba7062 | ||
|
|
a03d619e2f | ||
|
|
76d1f0bb1e | ||
|
|
2bad5a1184 | ||
|
|
94ba3aee05 | ||
|
|
563758f69f | ||
|
|
56af85cc3e | ||
|
|
6a1a861ecc | ||
|
|
ceab574a22 | ||
|
|
98704fdb28 | ||
|
|
fd5cba5219 | ||
|
|
be5aaa2b66 | ||
|
|
7e8687decd | ||
|
|
4c96324ef7 | ||
|
|
dd3c81ec5f | ||
|
|
42f0b5f8fc | ||
|
|
11b2cd88b7 | ||
|
|
6bf98f6db3 | ||
|
|
10b4e3c634 | ||
|
|
a3f5223b4c | ||
|
|
2855575b36 | ||
|
|
1f0ba20523 | ||
|
|
2f53416e09 | ||
|
|
ddbf266a3f | ||
|
|
d815415f36 | ||
|
|
cdacc56fd7 | ||
|
|
455d909c74 | ||
|
|
52d84afed6 | ||
|
|
f06d1d4d9a | ||
|
|
805a65bbaa | ||
|
|
f217950b13 | ||
|
|
9ff65441ef | ||
|
|
2b20282a41 | ||
|
|
96ad2de896 | ||
|
|
e1ea875c21 | ||
|
|
500e91977c | ||
|
|
bd194ff955 | ||
|
|
828bd71f22 | ||
|
|
5991f692b2 | ||
|
|
200d78a140 | ||
|
|
9a502b5e47 | ||
|
|
97ef3772ea | ||
|
|
eb18be200e | ||
|
|
467e97ff4b | ||
|
|
27b802d3c2 | ||
|
|
37b0a175f7 | ||
|
|
b2b79f12a2 | ||
|
|
885c578582 | ||
|
|
e61e4b109a | ||
|
|
f3bafbeb52 | ||
|
|
e55c0cdcef | ||
|
|
e73bbf4d6a | ||
|
|
3859289218 | ||
|
|
591bb45a4e | ||
|
|
b31f518fca | ||
|
|
dfbdb989db | ||
|
|
f194ebbc20 | ||
|
|
ab0e7e1e07 | ||
|
|
d809f50c0e | ||
|
|
a48d24de26 | ||
|
|
0dacc20e74 | ||
|
|
08df6cb4f8 | ||
|
|
0676ac8942 | ||
|
|
c257e8f0fe | ||
|
|
521670f683 | ||
|
|
87216b5d91 | ||
|
|
e6122a3d36 | ||
|
|
e6e1502308 | ||
|
|
7f5be3a688 | ||
|
|
4dde49a9f0 | ||
|
|
ce830b692b | ||
|
|
563472f3a9 | ||
|
|
14acd45927 | ||
|
|
9e2c7a08df | ||
|
|
f10c8dc379 | ||
|
|
fdd815879a | ||
|
|
635f238576 | ||
|
|
615e337e3f | ||
|
|
acd5d4b192 | ||
|
|
9a41b697c6 | ||
|
|
5cb67e00a6 | ||
|
|
350f13e97c | ||
|
|
4d6cbf5073 | ||
|
|
8d7b10d21e | ||
|
|
6753a93c0d | ||
|
|
9ee763337d | ||
|
|
ace0cb7823 | ||
|
|
44e518ef03 | ||
|
|
e28b96b45e | ||
|
|
11427a980c | ||
|
|
cb95562e58 |
@@ -15,6 +15,7 @@ module.exports = {
|
|||||||
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
|
||||||
'react/prop-types': 'off',
|
'react/prop-types': 'off',
|
||||||
'simple-import-sort/imports': 'error',
|
'simple-import-sort/imports': 'error',
|
||||||
'simple-import-sort/exports': 'error'
|
'simple-import-sort/exports': 'error',
|
||||||
|
'react/no-is-mounted': 'off'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
3
.vscode/settings.json
vendored
@@ -29,5 +29,6 @@
|
|||||||
},
|
},
|
||||||
"[markdown]": {
|
"[markdown]": {
|
||||||
"files.trimTrailingWhitespace": false
|
"files.trimTrailingWhitespace": false
|
||||||
}
|
},
|
||||||
|
"i18n-ally.localesPaths": ["src/renderer/src/i18n"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ files:
|
|||||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||||
- '!src/*'
|
- '!src'
|
||||||
- '!local'
|
- '!local'
|
||||||
|
- '!scripts'
|
||||||
|
- '!resources'
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
- resources/**
|
- resources/**
|
||||||
win:
|
win:
|
||||||
@@ -57,5 +59,11 @@ electronDownload:
|
|||||||
afterSign: scripts/notarize.js
|
afterSign: scripts/notarize.js
|
||||||
releaseInfo:
|
releaseInfo:
|
||||||
releaseNotes: |
|
releaseNotes: |
|
||||||
增加应用备份和恢复功能
|
本次更新:
|
||||||
增加更多AI小程序
|
增加了30多种文本文档格式选择
|
||||||
|
支持粘贴图片和文件到聊天输入框
|
||||||
|
支持将对话移动到其他智能体了
|
||||||
|
近期更新:
|
||||||
|
支持 Vision 模型
|
||||||
|
新增文件功能
|
||||||
|
支持从特定消息创建新分支
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export default defineConfig({
|
|||||||
plugins: [externalizeDepsPlugin()],
|
plugins: [externalizeDepsPlugin()],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
ollama: resolve('ollama/src')
|
'@types': resolve('src/renderer/src/types'),
|
||||||
|
'@main': resolve('src/main')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
13
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "CherryStudio",
|
"name": "CherryStudio",
|
||||||
"version": "0.6.2",
|
"version": "0.7.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A powerful AI assistant for producer.",
|
"description": "A powerful AI assistant for producer.",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
@@ -54,9 +54,12 @@
|
|||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
|
"dexie": "^4.0.8",
|
||||||
|
"dexie-react-hooks": "^1.1.7",
|
||||||
"dotenv-cli": "^7.4.2",
|
"dotenv-cli": "^7.4.2",
|
||||||
"electron": "^28.3.3",
|
"electron": "^28.3.3",
|
||||||
"electron-builder": "^24.9.1",
|
"electron-builder": "^24.9.1",
|
||||||
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-vite": "^2.0.0",
|
"electron-vite": "^2.0.0",
|
||||||
"emittery": "^1.0.3",
|
"emittery": "^1.0.3",
|
||||||
"emoji-picker-element": "^1.22.1",
|
"emoji-picker-element": "^1.22.1",
|
||||||
@@ -65,10 +68,11 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^4.6.2",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
"eslint-plugin-unused-imports": "^4.0.0",
|
"eslint-plugin-unused-imports": "^4.0.0",
|
||||||
"gpt-tokens": "^1.3.6",
|
"gpt-tokens": "^1.3.10",
|
||||||
"i18next": "^23.11.5",
|
"i18next": "^23.11.5",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"mime": "^4.0.4",
|
||||||
"openai": "^4.52.1",
|
"openai": "^4.52.1",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@@ -87,7 +91,7 @@
|
|||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"sass": "^1.77.2",
|
"sass": "^1.77.2",
|
||||||
"styled-components": "^6.1.11",
|
"styled-components": "^6.1.11",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.6.2",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"vite": "^5.0.12"
|
"vite": "^5.0.12"
|
||||||
},
|
},
|
||||||
@@ -96,8 +100,7 @@
|
|||||||
"react-dom": "^17.0.0 || ^18.0.0"
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@electron/notarize": "2.3.2",
|
|
||||||
"@electron/notarize@npm:2.2.1": "patch:@electron/notarize@npm%3A2.3.2#~/.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch"
|
"@electron/notarize@npm:2.2.1": "patch:@electron/notarize@npm%3A2.3.2#~/.yarn/patches/@electron-notarize-npm-2.3.2-535908a4bd.patch"
|
||||||
},
|
},
|
||||||
"packageManager": "yarn@4.3.1"
|
"packageManager": "yarn@4.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,22 @@
|
|||||||
|
import fs from 'node:fs'
|
||||||
|
|
||||||
|
import { app } from 'electron'
|
||||||
import Store from 'electron-store'
|
import Store from 'electron-store'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
const isDev = process.env.NODE_ENV === 'development'
|
||||||
|
|
||||||
|
isDev && app.setPath('userData', app.getPath('userData') + 'Dev')
|
||||||
|
|
||||||
|
const getDataPath = () => {
|
||||||
|
const dataPath = path.join(app.getPath('userData'), 'Data')
|
||||||
|
if (!fs.existsSync(dataPath)) {
|
||||||
|
fs.mkdirSync(dataPath, { recursive: true })
|
||||||
|
}
|
||||||
|
return dataPath
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DATA_PATH = getDataPath()
|
||||||
|
|
||||||
export const appConfig = new Store()
|
export const appConfig = new Store()
|
||||||
|
|
||||||
|
|||||||
9
src/main/env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
interface ImportMetaEnv {
|
||||||
|
VITE_MAIN_BUNDLE_ID: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImportMeta {
|
||||||
|
readonly env: ImportMetaEnv
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||||
import { app, BrowserWindow } from 'electron'
|
import { app, BrowserWindow } from 'electron'
|
||||||
|
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||||
|
|
||||||
import { registerIpc } from './ipc'
|
import { registerIpc } from './ipc'
|
||||||
import { updateUserDataPath } from './utils/upgrade'
|
import { updateUserDataPath } from './utils/upgrade'
|
||||||
@@ -12,7 +13,7 @@ app.whenReady().then(async () => {
|
|||||||
await updateUserDataPath()
|
await updateUserDataPath()
|
||||||
|
|
||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId('com.kangfenmao.CherryStudio')
|
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||||
|
|
||||||
// Default open or close DevTools by F12 in development
|
// Default open or close DevTools by F12 in development
|
||||||
// and ignore CommandOrControl + R in production.
|
// and ignore CommandOrControl + R in production.
|
||||||
@@ -30,6 +31,12 @@ app.whenReady().then(async () => {
|
|||||||
const mainWindow = createMainWindow()
|
const mainWindow = createMainWindow()
|
||||||
|
|
||||||
registerIpc(mainWindow, app)
|
registerIpc(mainWindow, app)
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
installExtension(REDUX_DEVTOOLS)
|
||||||
|
.then((name) => console.log(`Added Extension: ${name}`))
|
||||||
|
.catch((err) => console.log('An error occurred: ', err))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Quit when all windows are closed, except on macOS. There, it's common
|
// Quit when all windows are closed, except on macOS. There, it's common
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
import { FileType } from '@types'
|
||||||
|
import { BrowserWindow, ipcMain, OpenDialogOptions, session, shell } from 'electron'
|
||||||
|
|
||||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||||
import AppUpdater from './updater'
|
import AppUpdater from './services/AppUpdater'
|
||||||
|
import FileManager from './services/FileManager'
|
||||||
import { openFile, saveFile } from './utils/file'
|
import { openFile, saveFile } from './utils/file'
|
||||||
import { compress, decompress } from './utils/zip'
|
import { compress, decompress } from './utils/zip'
|
||||||
import { createMinappWindow } from './window'
|
import { createMinappWindow } from './window'
|
||||||
|
|
||||||
|
const fileManager = new FileManager()
|
||||||
|
|
||||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||||
const { autoUpdater } = new AppUpdater(mainWindow)
|
const { autoUpdater } = new AppUpdater(mainWindow)
|
||||||
|
|
||||||
@@ -31,6 +35,19 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
|
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
|
||||||
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
|
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
|
||||||
|
|
||||||
|
ipcMain.handle('file:base64Image', async (_, id) => await fileManager.base64Image(id))
|
||||||
|
ipcMain.handle('file:select', async (_, options?: OpenDialogOptions) => await fileManager.selectFile(options))
|
||||||
|
ipcMain.handle('file:upload', async (_, file: FileType) => await fileManager.uploadFile(file))
|
||||||
|
ipcMain.handle('file:clear', async () => await fileManager.clear())
|
||||||
|
ipcMain.handle('file:read', async (_, id: string) => await fileManager.readFile(id))
|
||||||
|
ipcMain.handle('file:delete', async (_, id: string) => await fileManager.deleteFile(id))
|
||||||
|
ipcMain.handle('file:get', async (_, filePath: string) => await fileManager.getFile(filePath))
|
||||||
|
ipcMain.handle('file:create', async (_, fileName: string) => await fileManager.createTempFile(fileName))
|
||||||
|
ipcMain.handle(
|
||||||
|
'file:write',
|
||||||
|
async (_, filePath: string, data: Uint8Array | string) => await fileManager.writeFile(filePath, data)
|
||||||
|
)
|
||||||
|
|
||||||
ipcMain.handle('minapp', (_, args) => {
|
ipcMain.handle('minapp', (_, args) => {
|
||||||
createMinappWindow({
|
createMinappWindow({
|
||||||
url: args.url,
|
url: args.url,
|
||||||
|
|||||||
198
src/main/services/FileManager.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
import { getFileType } from '@main/utils/file'
|
||||||
|
import { FileType } from '@types'
|
||||||
|
import * as crypto from 'crypto'
|
||||||
|
import { app, dialog, OpenDialogOptions } from 'electron'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
|
class FileManager {
|
||||||
|
private storageDir: string
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
|
||||||
|
this.initStorageDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
private initStorageDir(): void {
|
||||||
|
if (!fs.existsSync(this.storageDir)) {
|
||||||
|
fs.mkdirSync(this.storageDir, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getFileHash(filePath: string): Promise<string> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = crypto.createHash('md5')
|
||||||
|
const stream = fs.createReadStream(filePath)
|
||||||
|
stream.on('data', (data) => hash.update(data))
|
||||||
|
stream.on('end', () => resolve(hash.digest('hex')))
|
||||||
|
stream.on('error', reject)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async findDuplicateFile(filePath: string): Promise<FileType | null> {
|
||||||
|
const stats = fs.statSync(filePath)
|
||||||
|
const fileSize = stats.size
|
||||||
|
|
||||||
|
const files = await fs.promises.readdir(this.storageDir)
|
||||||
|
for (const file of files) {
|
||||||
|
const storedFilePath = path.join(this.storageDir, file)
|
||||||
|
const storedStats = fs.statSync(storedFilePath)
|
||||||
|
|
||||||
|
if (storedStats.size === fileSize) {
|
||||||
|
const [originalHash, storedHash] = await Promise.all([
|
||||||
|
this.getFileHash(filePath),
|
||||||
|
this.getFileHash(storedFilePath)
|
||||||
|
])
|
||||||
|
|
||||||
|
if (originalHash === storedHash) {
|
||||||
|
const ext = path.extname(file)
|
||||||
|
const id = path.basename(file, ext)
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
origin_name: file,
|
||||||
|
name: file + ext,
|
||||||
|
path: storedFilePath,
|
||||||
|
created_at: storedStats.birthtime,
|
||||||
|
size: storedStats.size,
|
||||||
|
ext,
|
||||||
|
type: getFileType(ext),
|
||||||
|
count: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectFile(options?: OpenDialogOptions): Promise<FileType[] | null> {
|
||||||
|
const defaultOptions: OpenDialogOptions = {
|
||||||
|
properties: ['openFile']
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogOptions = { ...defaultOptions, ...options }
|
||||||
|
|
||||||
|
const result = await dialog.showOpenDialog(dialogOptions)
|
||||||
|
|
||||||
|
if (result.canceled || result.filePaths.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileMetadataPromises = result.filePaths.map(async (filePath) => {
|
||||||
|
const stats = fs.statSync(filePath)
|
||||||
|
const ext = path.extname(filePath)
|
||||||
|
const fileType = getFileType(ext)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: uuidv4(),
|
||||||
|
origin_name: path.basename(filePath),
|
||||||
|
name: path.basename(filePath),
|
||||||
|
path: filePath,
|
||||||
|
created_at: stats.birthtime,
|
||||||
|
size: stats.size,
|
||||||
|
ext: ext,
|
||||||
|
type: fileType,
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return Promise.all(fileMetadataPromises)
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadFile(file: FileType): Promise<FileType> {
|
||||||
|
const duplicateFile = await this.findDuplicateFile(file.path)
|
||||||
|
|
||||||
|
if (duplicateFile) {
|
||||||
|
return duplicateFile
|
||||||
|
}
|
||||||
|
|
||||||
|
const uuid = uuidv4()
|
||||||
|
const origin_name = path.basename(file.path)
|
||||||
|
const ext = path.extname(origin_name)
|
||||||
|
const destPath = path.join(this.storageDir, uuid + ext)
|
||||||
|
|
||||||
|
await fs.promises.copyFile(file.path, destPath)
|
||||||
|
const stats = await fs.promises.stat(destPath)
|
||||||
|
const fileType = getFileType(ext)
|
||||||
|
|
||||||
|
const fileMetadata: FileType = {
|
||||||
|
id: uuid,
|
||||||
|
origin_name,
|
||||||
|
name: uuid + ext,
|
||||||
|
path: destPath,
|
||||||
|
created_at: stats.birthtime,
|
||||||
|
size: stats.size,
|
||||||
|
ext: ext,
|
||||||
|
type: fileType,
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFile(filePath: string): Promise<FileType | null> {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const stats = fs.statSync(filePath)
|
||||||
|
const ext = path.extname(filePath)
|
||||||
|
const fileType = getFileType(ext)
|
||||||
|
|
||||||
|
const fileInfo: FileType = {
|
||||||
|
id: uuidv4(),
|
||||||
|
origin_name: path.basename(filePath),
|
||||||
|
name: path.basename(filePath),
|
||||||
|
path: filePath,
|
||||||
|
created_at: stats.birthtime,
|
||||||
|
size: stats.size,
|
||||||
|
ext: ext,
|
||||||
|
type: fileType,
|
||||||
|
count: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(id: string): Promise<void> {
|
||||||
|
await fs.promises.unlink(path.join(this.storageDir, id))
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFile(id: string): Promise<string> {
|
||||||
|
const filePath = path.join(this.storageDir, id)
|
||||||
|
return fs.readFileSync(filePath, 'utf8')
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTempFile(fileName: string): Promise<string> {
|
||||||
|
const tempDir = path.join(app.getPath('temp'), 'CherryStudio')
|
||||||
|
if (!fs.existsSync(tempDir)) {
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true })
|
||||||
|
}
|
||||||
|
const tempFilePath = path.join(tempDir, `temp_file_${uuidv4()}_${fileName}`)
|
||||||
|
return tempFilePath
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeFile(filePath: string, data: Uint8Array | string): Promise<void> {
|
||||||
|
await fs.promises.writeFile(filePath, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async base64Image(id: string): Promise<{ mime: string; base64: string; data: string }> {
|
||||||
|
const filePath = path.join(this.storageDir, id)
|
||||||
|
const data = await fs.promises.readFile(filePath)
|
||||||
|
const base64 = data.toString('base64')
|
||||||
|
const mime = `image/${path.extname(filePath).slice(1)}`
|
||||||
|
return {
|
||||||
|
mime,
|
||||||
|
base64,
|
||||||
|
data: `data:${mime};base64,${base64}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async clear(): Promise<void> {
|
||||||
|
await fs.promises.rmdir(this.storageDir, { recursive: true })
|
||||||
|
await this.initStorageDir()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileManager
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { dialog, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
import { dialog, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
|
||||||
import logger from 'electron-log'
|
import logger from 'electron-log'
|
||||||
import { writeFile } from 'fs'
|
import { writeFileSync } from 'fs'
|
||||||
import { readFile } from 'fs/promises'
|
import { readFile } from 'fs/promises'
|
||||||
|
|
||||||
|
import { FileTypes } from '../../renderer/src/types'
|
||||||
|
|
||||||
export async function saveFile(
|
export async function saveFile(
|
||||||
_: Electron.IpcMainInvokeEvent,
|
_: Electron.IpcMainInvokeEvent,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
@@ -17,11 +19,7 @@ export async function saveFile(
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (!result.canceled && result.filePath) {
|
if (!result.canceled && result.filePath) {
|
||||||
writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => {
|
await writeFileSync(result.filePath, content, { encoding: 'utf-8' })
|
||||||
if (err) {
|
|
||||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||||
@@ -53,3 +51,108 @@ export async function openFile(
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFileType(ext: string): FileTypes {
|
||||||
|
const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||||
|
const videoExts = ['.mp4', '.avi', '.mov', '.wmv', '.flv', '.mkv']
|
||||||
|
const audioExts = ['.mp3', '.wav', '.ogg', '.flac', '.aac']
|
||||||
|
const documentExts = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx']
|
||||||
|
const textExts = [
|
||||||
|
'.txt', // 普通文本文件
|
||||||
|
'.md', // Markdown 文件
|
||||||
|
'.mdx', // Markdown 文件
|
||||||
|
'.html', // HTML 文件
|
||||||
|
'.htm', // HTML 文件的另一种扩展名
|
||||||
|
'.xml', // XML 文件
|
||||||
|
'.json', // JSON 文件
|
||||||
|
'.yaml', // YAML 文件
|
||||||
|
'.yml', // YAML 文件的另一种扩展名
|
||||||
|
'.csv', // 逗号分隔值文件
|
||||||
|
'.tsv', // 制表符分隔值文件
|
||||||
|
'.ini', // 配置文件
|
||||||
|
'.log', // 日志文件
|
||||||
|
'.rtf', // 富文本格式文件
|
||||||
|
'.tex', // LaTeX 文件
|
||||||
|
'.srt', // 字幕文件
|
||||||
|
'.xhtml', // XHTML 文件
|
||||||
|
'.nfo', // 信息文件(主要用于场景发布)
|
||||||
|
'.conf', // 配置文件
|
||||||
|
'.config', // 配置文件
|
||||||
|
'.env', // 环境变量文件
|
||||||
|
'.properties', // 配置属性文件
|
||||||
|
'.latex', // LaTeX 文档文件
|
||||||
|
'.rst', // reStructuredText 文件
|
||||||
|
'.php', // PHP 脚本文件,包含嵌入的 HTML
|
||||||
|
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
|
||||||
|
'.ts', // TypeScript 文件
|
||||||
|
'.jsp', // JavaServer Pages 文件
|
||||||
|
'.aspx', // ASP.NET 文件
|
||||||
|
'.bat', // Windows 批处理文件
|
||||||
|
'.sh', // Unix/Linux Shell 脚本文件
|
||||||
|
'.py', // Python 脚本文件
|
||||||
|
'.rb', // Ruby 脚本文件
|
||||||
|
'.pl', // Perl 脚本文件
|
||||||
|
'.sql', // SQL 脚本文件
|
||||||
|
'.css', // Cascading Style Sheets 文件
|
||||||
|
'.less', // Less CSS 预处理器文件
|
||||||
|
'.scss', // Sass CSS 预处理器文件
|
||||||
|
'.sass', // Sass 文件
|
||||||
|
'.styl', // Stylus CSS 预处理器文件
|
||||||
|
'.coffee', // CoffeeScript 文件
|
||||||
|
'.ino', // Arduino 代码文件
|
||||||
|
'.ino', // Arduino 代码文件
|
||||||
|
'.asm', // Assembly 语言文件
|
||||||
|
'.go', // Go 语言文件
|
||||||
|
'.scala', // Scala 语言文件
|
||||||
|
'.swift', // Swift 语言文件
|
||||||
|
'.kt', // Kotlin 语言文件
|
||||||
|
'.rs', // Rust 语言文件
|
||||||
|
'.lua', // Lua 语言文件
|
||||||
|
'.groovy', // Groovy 语言文件
|
||||||
|
'.dart', // Dart 语言文件
|
||||||
|
'.hs', // Haskell 语言文件
|
||||||
|
'.clj', // Clojure 语言文件
|
||||||
|
'.cljs', // ClojureScript 语言文件
|
||||||
|
'.elm', // Elm 语言文件
|
||||||
|
'.erl', // Erlang 语言文件
|
||||||
|
'.ex', // Elixir 语言文件
|
||||||
|
'.exs', // Elixir 脚本文件
|
||||||
|
'.pug', // Pug (formerly Jade) 模板文件
|
||||||
|
'.haml', // Haml 模板文件
|
||||||
|
'.slim', // Slim 模板文件
|
||||||
|
'.tpl', // 模板文件(通用)
|
||||||
|
'.ejs', // Embedded JavaScript 模板文件
|
||||||
|
'.hbs', // Handlebars 模板文件
|
||||||
|
'.mustache', // Mustache 模板文件
|
||||||
|
'.jade', // Jade 模板文件 (已重命名为 Pug)
|
||||||
|
'.twig', // Twig 模板文件
|
||||||
|
'.blade', // Blade 模板文件 (Laravel)
|
||||||
|
'.vue', // Vue.js 单文件组件
|
||||||
|
'.jsx', // React JSX 文件
|
||||||
|
'.tsx', // React TSX 文件
|
||||||
|
'.graphql', // GraphQL 查询语言文件
|
||||||
|
'.gql', // GraphQL 查询语言文件
|
||||||
|
'.proto', // Protocol Buffers 文件
|
||||||
|
'.thrift', // Thrift 文件
|
||||||
|
'.toml', // TOML 配置文件
|
||||||
|
'.edn', // Clojure 数据表示文件
|
||||||
|
'.cake', // CakePHP 配置文件
|
||||||
|
'.ctp', // CakePHP 视图文件
|
||||||
|
'.cfm', // ColdFusion 标记语言文件
|
||||||
|
'.cfc', // ColdFusion 组件文件
|
||||||
|
'.m', // Objective-C 源文件
|
||||||
|
'.mm', // Objective-C++ 源文件
|
||||||
|
'.gradle', // Gradle 构建文件
|
||||||
|
'.groovy', // Gradle 构建文件
|
||||||
|
'.gradle', // Gradle 构建文件
|
||||||
|
'.kts' // Kotlin Script 文件
|
||||||
|
]
|
||||||
|
|
||||||
|
ext = ext.toLowerCase()
|
||||||
|
if (imageExts.includes(ext)) return FileTypes.IMAGE
|
||||||
|
if (videoExts.includes(ext)) return FileTypes.VIDEO
|
||||||
|
if (audioExts.includes(ext)) return FileTypes.AUDIO
|
||||||
|
if (textExts.includes(ext)) return FileTypes.TEXT
|
||||||
|
if (documentExts.includes(ext)) return FileTypes.DOCUMENT
|
||||||
|
return FileTypes.OTHER
|
||||||
|
}
|
||||||
|
|||||||
7
src/main/utils/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
|
||||||
|
import { app } from 'electron'
|
||||||
|
|
||||||
|
export function getResourcePath() {
|
||||||
|
return path.join(app.getAppPath(), 'resources')
|
||||||
|
}
|
||||||
@@ -34,7 +34,8 @@ export function createMainWindow() {
|
|||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
sandbox: false,
|
sandbox: false,
|
||||||
webSecurity: false
|
webSecurity: false,
|
||||||
|
webviewTag: true
|
||||||
// devTools: !app.isPackaged,
|
// devTools: !app.isPackaged,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
12
src/preload/index.d.ts
vendored
@@ -1,4 +1,5 @@
|
|||||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||||
|
import { FileType } from '@renderer/types'
|
||||||
import type { OpenDialogOptions } from 'electron'
|
import type { OpenDialogOptions } from 'electron'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -20,6 +21,17 @@ declare global {
|
|||||||
reload: () => void
|
reload: () => void
|
||||||
compress: (text: string) => Promise<Buffer>
|
compress: (text: string) => Promise<Buffer>
|
||||||
decompress: (text: Buffer) => Promise<string>
|
decompress: (text: Buffer) => Promise<string>
|
||||||
|
file: {
|
||||||
|
select: (options?: OpenDialogOptions) => Promise<FileType[] | null>
|
||||||
|
upload: (file: FileType) => Promise<FileType>
|
||||||
|
delete: (fileId: string) => Promise<void>
|
||||||
|
read: (fileId: string) => Promise<string>
|
||||||
|
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
|
||||||
|
clear: () => Promise<void>
|
||||||
|
get: (filePath: string) => Promise<FileType | null>
|
||||||
|
create: (fileName: string) => Promise<string>
|
||||||
|
write: (filePath: string, data: Uint8Array | string) => Promise<void>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { electronAPI } from '@electron-toolkit/preload'
|
import { electronAPI } from '@electron-toolkit/preload'
|
||||||
import { contextBridge, ipcRenderer } from 'electron'
|
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||||
|
|
||||||
// Custom APIs for renderer
|
// Custom APIs for renderer
|
||||||
const api = {
|
const api = {
|
||||||
@@ -12,10 +12,21 @@ const api = {
|
|||||||
openFile: (options?: { decompress: boolean }) => ipcRenderer.invoke('open-file', options),
|
openFile: (options?: { decompress: boolean }) => ipcRenderer.invoke('open-file', options),
|
||||||
reload: () => ipcRenderer.invoke('reload'),
|
reload: () => ipcRenderer.invoke('reload'),
|
||||||
saveFile: (path: string, content: string, options?: { compress: boolean }) => {
|
saveFile: (path: string, content: string, options?: { compress: boolean }) => {
|
||||||
ipcRenderer.invoke('save-file', path, content, options)
|
return ipcRenderer.invoke('save-file', path, content, options)
|
||||||
},
|
},
|
||||||
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||||
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
|
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text),
|
||||||
|
file: {
|
||||||
|
select: (options?: OpenDialogOptions) => ipcRenderer.invoke('file:select', options),
|
||||||
|
upload: (filePath: string) => ipcRenderer.invoke('file:upload', filePath),
|
||||||
|
delete: (fileId: string) => ipcRenderer.invoke('file:delete', fileId),
|
||||||
|
read: (fileId: string) => ipcRenderer.invoke('file:read', fileId),
|
||||||
|
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId),
|
||||||
|
clear: () => ipcRenderer.invoke('file:clear'),
|
||||||
|
get: (filePath: string) => ipcRenderer.invoke('file:get', filePath),
|
||||||
|
create: (fileName: string) => ipcRenderer.invoke('file:create', fileName),
|
||||||
|
write: (filePath: string, data: Uint8Array | string) => ipcRenderer.invoke('file:write', filePath, data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data:; frame-src * file:" />
|
content="default-src 'self'; connect-src *; script-src 'self' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: *; frame-src * file:" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import '@renderer/databases'
|
||||||
|
|
||||||
import store, { persistor } from '@renderer/store'
|
import store, { persistor } from '@renderer/store'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
import { HashRouter, Route, Routes } from 'react-router-dom'
|
||||||
@@ -5,13 +7,14 @@ import { PersistGate } from 'redux-persist/integration/react'
|
|||||||
|
|
||||||
import Sidebar from './components/app/Sidebar'
|
import Sidebar from './components/app/Sidebar'
|
||||||
import TopViewContainer from './components/TopView'
|
import TopViewContainer from './components/TopView'
|
||||||
|
import AntdProvider from './context/AntdProvider'
|
||||||
|
import { ThemeProvider } from './context/ThemeProvider'
|
||||||
import AgentsPage from './pages/agents/AgentsPage'
|
import AgentsPage from './pages/agents/AgentsPage'
|
||||||
import AppsPage from './pages/apps/AppsPage'
|
import AppsPage from './pages/apps/AppsPage'
|
||||||
|
import FilesPage from './pages/files/FilesPage'
|
||||||
import HomePage from './pages/home/HomePage'
|
import HomePage from './pages/home/HomePage'
|
||||||
import SettingsPage from './pages/settings/SettingsPage'
|
import SettingsPage from './pages/settings/SettingsPage'
|
||||||
import TranslatePage from './pages/translate/TranslatePage'
|
import TranslatePage from './pages/translate/TranslatePage'
|
||||||
import AntdProvider from './providers/AntdProvider'
|
|
||||||
import { ThemeProvider } from './providers/ThemeProvider'
|
|
||||||
|
|
||||||
function App(): JSX.Element {
|
function App(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
@@ -24,6 +27,7 @@ function App(): JSX.Element {
|
|||||||
<Sidebar />
|
<Sidebar />
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/files" element={<FilesPage />} />
|
||||||
<Route path="/agents" element={<AgentsPage />} />
|
<Route path="/agents" element={<AgentsPage />} />
|
||||||
<Route path="/translate" element={<TranslatePage />} />
|
<Route path="/translate" element={<TranslatePage />} />
|
||||||
<Route path="/apps" element={<AppsPage />} />
|
<Route path="/apps" element={<AppsPage />} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'iconfont'; /* Project id 4563475 */
|
font-family: 'iconfont'; /* Project id 4563475 */
|
||||||
src: url('iconfont.woff2?t=1724204739157') format('woff2');
|
src: url('iconfont.woff2?t=1725606177995') format('woff2');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@@ -11,6 +11,30 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-a-darkmode:before {
|
||||||
|
content: '\e6cd';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ai-model:before {
|
||||||
|
content: '\e827';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-ai-model1:before {
|
||||||
|
content: '\ec09';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-gridlines:before {
|
||||||
|
content: '\e942';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-grid-row-2copy:before {
|
||||||
|
content: '\e681';
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-inbox:before {
|
||||||
|
content: '\e869';
|
||||||
|
}
|
||||||
|
|
||||||
.icon-business-smart-assistant:before {
|
.icon-business-smart-assistant:before {
|
||||||
content: '\e601';
|
content: '\e601';
|
||||||
}
|
}
|
||||||
@@ -39,11 +63,11 @@
|
|||||||
content: '\e758';
|
content: '\e758';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-hidesidebarhoriz:before {
|
.icon-hide-sidebar:before {
|
||||||
content: '\e8eb';
|
content: '\e8eb';
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-showsidebarhoriz:before {
|
.icon-show-sidebar:before {
|
||||||
content: '\e944';
|
content: '\e944';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
BIN
src/renderer/src/assets/images/apps/perplexity.webp
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src/renderer/src/assets/images/apps/poe.webp
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/renderer/src/assets/images/apps/zhihu.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/renderer/src/assets/images/avatar.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/renderer/src/assets/images/avatar.webp
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 9.1 KiB |
BIN
src/renderer/src/assets/images/models/chatglm.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
src/renderer/src/assets/images/models/cohere.webp
Normal file
|
After Width: | Height: | Size: 4.6 KiB |
BIN
src/renderer/src/assets/images/models/minicpm.webp
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
3
src/renderer/src/assets/images/providers/github.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M16 0C7.16 0 0 7.16 0 16C0 23.08 4.58 29.06 10.94 31.18C11.74 31.32 12.04 30.84 12.04 30.42C12.04 30.04 12.02 28.78 12.02 27.44C8 28.18 6.96 26.46 6.64 25.56C6.46 25.1 5.68 23.68 5 23.3C4.44 23 3.64 22.26 4.98 22.24C6.24 22.22 7.14 23.4 7.44 23.88C8.88 26.3 11.18 25.62 12.1 25.2C12.24 24.16 12.66 23.46 13.12 23.06C9.56 22.66 5.84 21.28 5.84 15.16C5.84 13.42 6.46 11.98 7.48 10.86C7.32 10.46 6.76 8.82 7.64 6.62C7.64 6.62 8.98 6.2 12.04 8.26C13.32 7.9 14.68 7.72 16.04 7.72C17.4 7.72 18.76 7.9 20.04 8.26C23.1 6.18 24.44 6.62 24.44 6.62C25.32 8.82 24.76 10.46 24.6 10.86C25.62 11.98 26.24 13.4 26.24 15.16C26.24 21.3 22.5 22.66 18.94 23.06C19.52 23.56 20.02 24.52 20.02 26.02C20.02 28.16 20 29.88 20 30.42C20 30.84 20.3 31.34 21.1 31.18C27.42 29.06 32 23.06 32 16C32 7.16 24.84 0 16 0V0Z" fill="#24292E"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 959 B |
@@ -24,20 +24,20 @@
|
|||||||
--color-background-soft: var(--color-black-soft);
|
--color-background-soft: var(--color-black-soft);
|
||||||
--color-background-mute: var(--color-black-mute);
|
--color-background-mute: var(--color-black-mute);
|
||||||
|
|
||||||
--color-primary: #135200;
|
--color-primary: #00b96b;
|
||||||
--color-primary-soft: #13520099;
|
--color-primary-soft: #00b96b99;
|
||||||
--color-primary-mute: #13520033;
|
--color-primary-mute: #00b96b33;
|
||||||
|
|
||||||
--color-text: var(--color-text-1);
|
--color-text: var(--color-text-1);
|
||||||
--color-icon: #ffffff99;
|
--color-icon: #ffffff99;
|
||||||
--color-icon-white: #ffffff;
|
--color-icon-white: #ffffff;
|
||||||
--color-border: #000;
|
--color-border: #ffffff20;
|
||||||
--color-border-soft: #ffffff20;
|
--color-border-soft: #ffffff20;
|
||||||
--color-error: #f44336;
|
--color-error: #f44336;
|
||||||
--color-link: #1677ff;
|
--color-link: #1677ff;
|
||||||
--color-code-background: #323232;
|
--color-code-background: #323232;
|
||||||
--color-scrollbar-thumb: rgba(255, 255, 255, 0.15);
|
--color-scrollbar-thumb: rgba(255, 255, 255, 0.08);
|
||||||
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3);
|
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.15);
|
||||||
|
|
||||||
--navbar-background-mac: rgba(30, 30, 30, 0.8);
|
--navbar-background-mac: rgba(30, 30, 30, 0.8);
|
||||||
--navbar-background: rgba(30, 30, 30);
|
--navbar-background: rgba(30, 30, 30);
|
||||||
@@ -48,9 +48,9 @@
|
|||||||
--status-bar-height: 40px;
|
--status-bar-height: 40px;
|
||||||
--input-bar-height: 85px;
|
--input-bar-height: 85px;
|
||||||
|
|
||||||
--assistants-width: 245px;
|
--assistants-width: 280px;
|
||||||
--topic-list-width: 260px;
|
--topic-list-width: 280px;
|
||||||
--settings-width: var(--assistants-width);
|
--settings-width: 260px;
|
||||||
}
|
}
|
||||||
|
|
||||||
body[theme-mode='light'] {
|
body[theme-mode='light'] {
|
||||||
@@ -86,8 +86,8 @@ body[theme-mode='light'] {
|
|||||||
--color-error: #f44336;
|
--color-error: #f44336;
|
||||||
--color-link: #1677ff;
|
--color-link: #1677ff;
|
||||||
--color-code-background: #e3e3e3;
|
--color-code-background: #e3e3e3;
|
||||||
--color-scrollbar-thumb: rgba(0, 0, 0, 0.15);
|
--color-scrollbar-thumb: rgba(0, 0, 0, 0.08);
|
||||||
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3);
|
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.15);
|
||||||
|
|
||||||
--navbar-background-mac: rgba(255, 255, 255, 0.75);
|
--navbar-background-mac: rgba(255, 255, 255, 0.75);
|
||||||
--navbar-background: rgba(255, 255, 255);
|
--navbar-background: rgba(255, 255, 255);
|
||||||
@@ -182,6 +182,12 @@ body,
|
|||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.minapp-drawer {
|
.minapp-drawer {
|
||||||
.ant-drawer-content-wrapper {
|
.ant-drawer-content-wrapper {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
@@ -204,3 +210,30 @@ body,
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-drawer-header {
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
.segmented-tab {
|
||||||
|
.ant-segmented-item-label {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.iconfont {
|
||||||
|
font-size: 13px;
|
||||||
|
margin-left: -2px;
|
||||||
|
}
|
||||||
|
.anticon-setting {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.icon-business-smart-assistant {
|
||||||
|
margin-right: -2px;
|
||||||
|
}
|
||||||
|
.ant-segmented-item-icon + * {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,6 +72,9 @@
|
|||||||
|
|
||||||
li {
|
li {
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
|
pre {
|
||||||
|
margin: 1.5em 0;
|
||||||
|
}
|
||||||
&::marker {
|
&::marker {
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
}
|
}
|
||||||
@@ -98,7 +101,8 @@
|
|||||||
font-family: 'Courier New', Courier, monospace;
|
font-family: 'Courier New', Courier, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
p code {
|
p code,
|
||||||
|
li code {
|
||||||
background: var(--color-background-mute);
|
background: var(--color-background-mute);
|
||||||
padding: 3px 5px;
|
padding: 3px 5px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@@ -106,18 +110,28 @@
|
|||||||
|
|
||||||
pre {
|
pre {
|
||||||
white-space: pre-wrap !important;
|
white-space: pre-wrap !important;
|
||||||
padding: 1em 0;
|
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-family: 'Fira Code', 'Courier New', Courier, monospace;
|
font-family: 'Fira Code', 'Courier New', Courier, monospace;
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
&:not(pre pre) {
|
||||||
|
> code:not(pre pre > code) {
|
||||||
|
padding: 15px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
pre {
|
pre {
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
code {
|
}
|
||||||
background: none;
|
|
||||||
padding: 0;
|
pre + pre {
|
||||||
border-radius: 0;
|
margin-top: 10px;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blockquote {
|
blockquote {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* 全局初始化滚动条样式 */
|
/* 全局初始化滚动条样式 */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 3px;
|
width: 2px;
|
||||||
height: 3px;
|
height: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
|
|||||||
@@ -1,18 +1,39 @@
|
|||||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
import {
|
||||||
|
DragDropContext,
|
||||||
|
Draggable,
|
||||||
|
Droppable,
|
||||||
|
DroppableProps,
|
||||||
|
DropResult,
|
||||||
|
OnDragEndResponder,
|
||||||
|
OnDragStartResponder,
|
||||||
|
ResponderProvided
|
||||||
|
} from '@hello-pangea/dnd'
|
||||||
import { droppableReorder } from '@renderer/utils'
|
import { droppableReorder } from '@renderer/utils'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
|
|
||||||
interface Props<T> {
|
interface Props<T> {
|
||||||
list: T[]
|
list: T[]
|
||||||
|
style?: React.CSSProperties
|
||||||
|
listStyle?: React.CSSProperties
|
||||||
children: (item: T, index: number) => React.ReactNode
|
children: (item: T, index: number) => React.ReactNode
|
||||||
onUpdate: (list: T[]) => void
|
onUpdate: (list: T[]) => void
|
||||||
onDragStart?: () => void
|
onDragStart?: OnDragStartResponder
|
||||||
onDragEnd?: () => void
|
onDragEnd?: OnDragEndResponder
|
||||||
|
droppableProps?: Partial<DroppableProps>
|
||||||
}
|
}
|
||||||
|
|
||||||
const DragableList: FC<Props<any>> = ({ children, list, onDragStart, onUpdate, onDragEnd }) => {
|
const DragableList: FC<Props<any>> = ({
|
||||||
const _onDragEnd = (result: DropResult) => {
|
children,
|
||||||
onDragEnd?.()
|
list,
|
||||||
|
style,
|
||||||
|
listStyle,
|
||||||
|
droppableProps,
|
||||||
|
onDragStart,
|
||||||
|
onUpdate,
|
||||||
|
onDragEnd
|
||||||
|
}) => {
|
||||||
|
const _onDragEnd = (result: DropResult, provided: ResponderProvided) => {
|
||||||
|
onDragEnd?.(result, provided)
|
||||||
if (result.destination) {
|
if (result.destination) {
|
||||||
const sourceIndex = result.source.index
|
const sourceIndex = result.source.index
|
||||||
const destIndex = result.destination.index
|
const destIndex = result.destination.index
|
||||||
@@ -23,17 +44,17 @@ const DragableList: FC<Props<any>> = ({ children, list, onDragStart, onUpdate, o
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
|
<DragDropContext onDragStart={onDragStart} onDragEnd={_onDragEnd}>
|
||||||
<Droppable droppableId="droppable">
|
<Droppable droppableId="droppable" {...droppableProps}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}>
|
||||||
{list.map((item, index) => (
|
{list.map((item, index) => (
|
||||||
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index}>
|
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index} {...droppableProps}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<div
|
<div
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
style={{ ...provided.draggableProps.style, marginBottom: 8 }}>
|
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
|
||||||
{children(item, index)}
|
{children(item, index)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { FC, useEffect, useRef } from 'react'
|
import { FC, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
15
src/renderer/src/components/Icons/VisionIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { EyeOutlined } from '@ant-design/icons'
|
||||||
|
import React, { FC } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const VisionIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
|
||||||
|
return <Icon {...(props as any)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = styled(EyeOutlined)`
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-left: 4px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default VisionIcon
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable react/no-unknown-property */
|
||||||
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
|
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
import { isMac, isWindows } from '@renderer/config/constant'
|
import { isMac, isWindows } from '@renderer/config/constant'
|
||||||
import { useBridge } from '@renderer/hooks/useBridge'
|
import { useBridge } from '@renderer/hooks/useBridge'
|
||||||
@@ -5,7 +6,8 @@ import store from '@renderer/store'
|
|||||||
import { setMinappShow } from '@renderer/store/runtime'
|
import { setMinappShow } from '@renderer/store/runtime'
|
||||||
import { MinAppType } from '@renderer/types'
|
import { MinAppType } from '@renderer/types'
|
||||||
import { Drawer } from 'antd'
|
import { Drawer } from 'antd'
|
||||||
import { useRef, useState } from 'react'
|
import { WebviewTag } from 'electron'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import { TopView } from '../TopView'
|
import { TopView } from '../TopView'
|
||||||
@@ -17,7 +19,7 @@ interface Props {
|
|||||||
|
|
||||||
const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
const webviewRef = useRef<WebviewTag | null>(null)
|
||||||
|
|
||||||
useBridge()
|
useBridge()
|
||||||
|
|
||||||
@@ -28,9 +30,11 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
|||||||
setTimeout(() => resolve({}), 300)
|
setTimeout(() => resolve({}), 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MinApp.onClose = onClose
|
||||||
|
|
||||||
const onReload = () => {
|
const onReload = () => {
|
||||||
if (iframeRef.current) {
|
if (webviewRef.current) {
|
||||||
iframeRef.current.src = app.url
|
webviewRef.current.src = app.url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +63,27 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const webview = webviewRef.current
|
||||||
|
|
||||||
|
if (webview) {
|
||||||
|
const handleNewWindow = (event: any) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (webview.loadURL) {
|
||||||
|
webview.loadURL(event.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webview.addEventListener('new-window', handleNewWindow)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
webview.removeEventListener('new-window', handleNewWindow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer
|
<Drawer
|
||||||
title={<Title />}
|
title={<Title />}
|
||||||
@@ -72,17 +97,17 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
|||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
style={{ marginLeft: 'var(--sidebar-width)' }}>
|
style={{ marginLeft: 'var(--sidebar-width)' }}>
|
||||||
<Frame src={app.url} ref={iframeRef} />
|
<webview src={app.url} ref={webviewRef} style={WebviewStyle} allowpopups={'true' as any} />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Frame = styled.iframe`
|
const WebviewStyle: React.CSSProperties = {
|
||||||
width: calc(100vw - var(--sidebar-width));
|
width: 'calc(100vw - var(--sidebar-width))',
|
||||||
height: calc(100vh - var(--navbar-height));
|
height: 'calc(100vh - var(--navbar-height))',
|
||||||
border: none;
|
backgroundColor: 'white',
|
||||||
background-color: white;
|
display: 'inline-flex'
|
||||||
`
|
}
|
||||||
|
|
||||||
const TitleContainer = styled.div`
|
const TitleContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -140,6 +165,7 @@ const Button = styled.div`
|
|||||||
|
|
||||||
export default class MinApp {
|
export default class MinApp {
|
||||||
static topviewId = 0
|
static topviewId = 0
|
||||||
|
static onClose = () => {}
|
||||||
static close() {
|
static close() {
|
||||||
TopView.hide('MinApp')
|
TopView.hide('MinApp')
|
||||||
store.dispatch(setMinappShow(false))
|
store.dispatch(setMinappShow(false))
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import systemAgents from '@renderer/config/agents.json'
|
|||||||
import { useAgents } from '@renderer/hooks/useAgents'
|
import { useAgents } from '@renderer/hooks/useAgents'
|
||||||
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { covertAgentToAssistant } from '@renderer/services/assistant'
|
import { covertAgentToAssistant } from '@renderer/services/assistant'
|
||||||
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
import { Agent, Assistant } from '@renderer/types'
|
import { Agent, Assistant } from '@renderer/types'
|
||||||
import { Input, Modal, Tag } from 'antd'
|
import { Input, Modal, Tag } from 'antd'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
@@ -50,6 +51,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
const assistant = covertAgentToAssistant(agent)
|
const assistant = covertAgentToAssistant(agent)
|
||||||
|
|
||||||
addAssistant(assistant)
|
addAssistant(assistant)
|
||||||
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_ASSISTANTS), 0)
|
||||||
resolve(assistant)
|
resolve(assistant)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
@@ -65,13 +67,13 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
style={{ marginTop: '5vh' }}
|
centered
|
||||||
title={t('chat.add.assistant.title')}
|
title={t('chat.add.assistant.title')}
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
afterClose={onClose}
|
afterClose={onClose}
|
||||||
transitionName=""
|
transitionName="ant-move-down"
|
||||||
maskTransitionName=""
|
maskTransitionName="ant-fade"
|
||||||
footer={null}>
|
footer={null}>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('common.search')}
|
placeholder={t('common.search')}
|
||||||
|
|||||||
@@ -34,7 +34,15 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title={assistant.name} open={open} onOk={onOk} onCancel={handleCancel} afterClose={onClose}>
|
<Modal
|
||||||
|
title={assistant.name}
|
||||||
|
open={open}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
transitionName="ant-move-down"
|
||||||
|
maskTransitionName="ant-fade"
|
||||||
|
centered>
|
||||||
<Box mb={8}>{t('common.name')}</Box>
|
<Box mb={8}>{t('common.name')}</Box>
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('common.assistant') + t('common.name')}
|
placeholder={t('common.assistant') + t('common.name')}
|
||||||
@@ -45,7 +53,7 @@ const AssistantSettingPopupContainer: React.FC<Props> = ({ assistant, resolve })
|
|||||||
{t('common.prompt')}
|
{t('common.prompt')}
|
||||||
</Box>
|
</Box>
|
||||||
<TextArea
|
<TextArea
|
||||||
rows={4}
|
rows={10}
|
||||||
placeholder={t('common.assistant') + t('common.prompt')}
|
placeholder={t('common.assistant') + t('common.prompt')}
|
||||||
value={prompt}
|
value={prompt}
|
||||||
onChange={(e) => setPrompt(e.target.value)}
|
onChange={(e) => setPrompt(e.target.value)}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
import useAvatar from '@renderer/hooks/useAvatar'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import LocalStorage from '@renderer/services/storage'
|
import ImageStorage from '@renderer/services/storage'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setAvatar } from '@renderer/store/runtime'
|
import { setAvatar } from '@renderer/store/runtime'
|
||||||
import { setUserName } from '@renderer/store/settings'
|
import { setUserName } from '@renderer/store/settings'
|
||||||
@@ -55,8 +55,8 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
try {
|
try {
|
||||||
const _file = file.originFileObj as File
|
const _file = file.originFileObj as File
|
||||||
const compressedFile = await compressImage(_file)
|
const compressedFile = await compressImage(_file)
|
||||||
await LocalStorage.storeImage('avatar', compressedFile)
|
await ImageStorage.set('avatar', compressedFile)
|
||||||
dispatch(setAvatar(await LocalStorage.getImage('avatar')))
|
dispatch(setAvatar(await ImageStorage.get('avatar')))
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
window.message.error(error.message)
|
window.message.error(error.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useRuntime } from '@renderer/hooks/useStore'
|
import { useRuntime } from '@renderer/hooks/useStore'
|
||||||
import { FC, PropsWithChildren } from 'react'
|
import { FC, PropsWithChildren } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
type Props = PropsWithChildren & JSX.IntrinsicElements['div']
|
type Props = PropsWithChildren & JSX.IntrinsicElements['div']
|
||||||
|
|
||||||
const navbarBackgroundColor = isMac ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
|
||||||
|
|
||||||
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
||||||
const { minappShow } = useRuntime()
|
const { minappShow } = useRuntime()
|
||||||
const backgroundColor = minappShow ? 'var(--navbar-background)' : navbarBackgroundColor
|
const { windowStyle } = useSettings()
|
||||||
|
|
||||||
|
const macTransparentWindow = isMac && windowStyle === 'transparent'
|
||||||
|
const navbarBgColor = macTransparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
||||||
|
const backgroundColor = minappShow ? 'var(--navbar-background)' : navbarBgColor
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavbarContainer {...props} style={{ backgroundColor }}>
|
<NavbarContainer {...props} style={{ backgroundColor }}>
|
||||||
@@ -39,7 +42,6 @@ const NavbarContainer = styled.div`
|
|||||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
|
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
|
||||||
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
|
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
|
||||||
border-bottom: 0.5px solid var(--color-border);
|
border-bottom: 0.5px solid var(--color-border);
|
||||||
background-color: ${navbarBackgroundColor};
|
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
`
|
`
|
||||||
@@ -64,7 +66,7 @@ const NavbarCenterContainer = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const NavbarRightContainer = styled.div`
|
const NavbarRightContainer = styled.div`
|
||||||
min-width: var(--settings-width);
|
min-width: var(--topic-list-width);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
|
|||||||
@@ -1,57 +1,81 @@
|
|||||||
import { TranslationOutlined } from '@ant-design/icons'
|
import { FolderOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
import { AppLogo, isLocalAi } from '@renderer/config/env'
|
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
import useAvatar from '@renderer/hooks/useAvatar'
|
||||||
import { useRuntime } from '@renderer/hooks/useStore'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useRuntime, useShowAssistants } from '@renderer/hooks/useStore'
|
||||||
import { Avatar } from 'antd'
|
import { Avatar } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { Link, useLocation } from 'react-router-dom'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import MinApp from '../MinApp'
|
||||||
import UserPopup from '../Popups/UserPopup'
|
import UserPopup from '../Popups/UserPopup'
|
||||||
|
|
||||||
const sidebarBackgroundColor = isMac ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
|
||||||
|
|
||||||
const Sidebar: FC = () => {
|
const Sidebar: FC = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const avatar = useAvatar()
|
const avatar = useAvatar()
|
||||||
const { minappShow } = useRuntime()
|
const { minappShow } = useRuntime()
|
||||||
|
const { toggleShowAssistants } = useShowAssistants()
|
||||||
|
const { generating } = useRuntime()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { windowStyle } = useSettings()
|
||||||
|
|
||||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||||
|
|
||||||
const onEditUser = () => {
|
const onEditUser = () => UserPopup.show()
|
||||||
UserPopup.show()
|
|
||||||
|
const macTransparentWindow = isMac && windowStyle === 'transparent'
|
||||||
|
const sidebarBgColor = macTransparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
||||||
|
|
||||||
|
const to = (path: string) => {
|
||||||
|
if (generating) {
|
||||||
|
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
navigate(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onToggleShowAssistants = () => {
|
||||||
|
pathname === '/' ? toggleShowAssistants() : navigate('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container style={{ backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBackgroundColor }}>
|
<Container style={{ backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBgColor }}>
|
||||||
<AvatarImg src={avatar || AppLogo} draggable={false} className="nodrag" onClick={onEditUser} />
|
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||||
<MainMenus>
|
<MainMenus>
|
||||||
<Menus>
|
<Menus onClick={MinApp.onClose}>
|
||||||
<StyledLink to="/">
|
<StyledLink onClick={onToggleShowAssistants}>
|
||||||
<Icon className={isRoute('/')}>
|
<Icon className={isRoute('/')}>
|
||||||
<i className="iconfont icon-chat"></i>
|
<i className="iconfont icon-chat"></i>
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
<StyledLink to="/agents">
|
<StyledLink onClick={() => to('/agents')}>
|
||||||
<Icon className={isRoute('/agents')}>
|
<Icon className={isRoute('/agents')}>
|
||||||
<i className="iconfont icon-business-smart-assistant"></i>
|
<i className="iconfont icon-business-smart-assistant"></i>
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
<StyledLink to="/translate">
|
<StyledLink onClick={() => to('/translate')}>
|
||||||
<Icon className={isRoute('/translate')}>
|
<Icon className={isRoute('/translate')}>
|
||||||
<TranslationOutlined />
|
<TranslationOutlined />
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
<StyledLink to="/apps">
|
<StyledLink onClick={() => to('/apps')}>
|
||||||
<Icon className={isRoute('/apps')}>
|
<Icon className={isRoute('/apps')}>
|
||||||
<i className="iconfont icon-appstore"></i>
|
<i className="iconfont icon-appstore"></i>
|
||||||
</Icon>
|
</Icon>
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
|
<StyledLink onClick={() => to('/files')}>
|
||||||
|
<Icon className={isRoute('/files')}>
|
||||||
|
<FolderOutlined />
|
||||||
|
</Icon>
|
||||||
|
</StyledLink>
|
||||||
</Menus>
|
</Menus>
|
||||||
</MainMenus>
|
</MainMenus>
|
||||||
<Menus>
|
<Menus onClick={MinApp.onClose}>
|
||||||
<StyledLink to={isLocalAi ? '/settings/assistant' : '/settings/provider'}>
|
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
|
||||||
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
||||||
<i className="iconfont icon-setting"></i>
|
<i className="iconfont icon-setting"></i>
|
||||||
</Icon>
|
</Icon>
|
||||||
@@ -72,8 +96,8 @@ const Container = styled.div`
|
|||||||
-webkit-app-region: drag !important;
|
-webkit-app-region: drag !important;
|
||||||
border-right: 0.5px solid var(--color-border);
|
border-right: 0.5px solid var(--color-border);
|
||||||
margin-top: ${isMac ? 'var(--navbar-height)' : 0};
|
margin-top: ${isMac ? 'var(--navbar-height)' : 0};
|
||||||
background-color: ${sidebarBackgroundColor};
|
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
|
z-index: 10000;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AvatarImg = styled(Avatar)`
|
const AvatarImg = styled(Avatar)`
|
||||||
@@ -133,7 +157,7 @@ const Icon = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const StyledLink = styled(Link)`
|
const StyledLink = styled.div`
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
-webkit-app-region: none;
|
-webkit-app-region: none;
|
||||||
&* {
|
&* {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export const DEFAULT_TEMPERATURE = 0.7
|
export const DEFAULT_TEMPERATURE = 0.7
|
||||||
export const DEFAULT_CONEXTCOUNT = 5
|
export const DEFAULT_CONEXTCOUNT = 6
|
||||||
export const DEFAULT_MAX_TOKENS = 4096
|
export const DEFAULT_MAX_TOKENS = 4096
|
||||||
export const FONT_FAMILY =
|
export const FONT_FAMILY =
|
||||||
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
|
"Ubuntu, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif"
|
||||||
@@ -7,3 +7,95 @@ export const platform = window.electron?.process?.platform
|
|||||||
export const isMac = platform === 'darwin'
|
export const isMac = platform === 'darwin'
|
||||||
export const isWindows = platform === 'win32' || platform === 'win64'
|
export const isWindows = platform === 'win32' || platform === 'win64'
|
||||||
export const isLinux = platform === 'linux'
|
export const isLinux = platform === 'linux'
|
||||||
|
|
||||||
|
export const imageExts = ['.jpg', '.png', '.jpeg']
|
||||||
|
export const textExts = [
|
||||||
|
'.txt', // 普通文本文件
|
||||||
|
'.md', // Markdown 文件
|
||||||
|
'.mdx', // Markdown 文件
|
||||||
|
'.html', // HTML 文件
|
||||||
|
'.htm', // HTML 文件的另一种扩展名
|
||||||
|
'.xml', // XML 文件
|
||||||
|
'.json', // JSON 文件
|
||||||
|
'.yaml', // YAML 文件
|
||||||
|
'.yml', // YAML 文件的另一种扩展名
|
||||||
|
'.csv', // 逗号分隔值文件
|
||||||
|
'.tsv', // 制表符分隔值文件
|
||||||
|
'.ini', // 配置文件
|
||||||
|
'.log', // 日志文件
|
||||||
|
'.rtf', // 富文本格式文件
|
||||||
|
'.tex', // LaTeX 文件
|
||||||
|
'.srt', // 字幕文件
|
||||||
|
'.xhtml', // XHTML 文件
|
||||||
|
'.nfo', // 信息文件(主要用于场景发布)
|
||||||
|
'.conf', // 配置文件
|
||||||
|
'.config', // 配置文件
|
||||||
|
'.env', // 环境变量文件
|
||||||
|
'.properties', // 配置属性文件
|
||||||
|
'.latex', // LaTeX 文档文件
|
||||||
|
'.rst', // reStructuredText 文件
|
||||||
|
'.php', // PHP 脚本文件,包含嵌入的 HTML
|
||||||
|
'.js', // JavaScript 文件(部分是文本,部分可能包含代码)
|
||||||
|
'.ts', // TypeScript 文件
|
||||||
|
'.jsp', // JavaServer Pages 文件
|
||||||
|
'.aspx', // ASP.NET 文件
|
||||||
|
'.bat', // Windows 批处理文件
|
||||||
|
'.sh', // Unix/Linux Shell 脚本文件
|
||||||
|
'.py', // Python 脚本文件
|
||||||
|
'.rb', // Ruby 脚本文件
|
||||||
|
'.pl', // Perl 脚本文件
|
||||||
|
'.sql', // SQL 脚本文件
|
||||||
|
'.css', // Cascading Style Sheets 文件
|
||||||
|
'.less', // Less CSS 预处理器文件
|
||||||
|
'.scss', // Sass CSS 预处理器文件
|
||||||
|
'.sass', // Sass 文件
|
||||||
|
'.styl', // Stylus CSS 预处理器文件
|
||||||
|
'.coffee', // CoffeeScript 文件
|
||||||
|
'.ino', // Arduino 代码文件
|
||||||
|
'.ino', // Arduino 代码文件
|
||||||
|
'.asm', // Assembly 语言文件
|
||||||
|
'.go', // Go 语言文件
|
||||||
|
'.scala', // Scala 语言文件
|
||||||
|
'.swift', // Swift 语言文件
|
||||||
|
'.kt', // Kotlin 语言文件
|
||||||
|
'.rs', // Rust 语言文件
|
||||||
|
'.lua', // Lua 语言文件
|
||||||
|
'.groovy', // Groovy 语言文件
|
||||||
|
'.dart', // Dart 语言文件
|
||||||
|
'.hs', // Haskell 语言文件
|
||||||
|
'.clj', // Clojure 语言文件
|
||||||
|
'.cljs', // ClojureScript 语言文件
|
||||||
|
'.elm', // Elm 语言文件
|
||||||
|
'.erl', // Erlang 语言文件
|
||||||
|
'.ex', // Elixir 语言文件
|
||||||
|
'.exs', // Elixir 脚本文件
|
||||||
|
'.pug', // Pug (formerly Jade) 模板文件
|
||||||
|
'.haml', // Haml 模板文件
|
||||||
|
'.slim', // Slim 模板文件
|
||||||
|
'.tpl', // 模板文件(通用)
|
||||||
|
'.ejs', // Embedded JavaScript 模板文件
|
||||||
|
'.hbs', // Handlebars 模板文件
|
||||||
|
'.mustache', // Mustache 模板文件
|
||||||
|
'.jade', // Jade 模板文件 (已重命名为 Pug)
|
||||||
|
'.twig', // Twig 模板文件
|
||||||
|
'.blade', // Blade 模板文件 (Laravel)
|
||||||
|
'.vue', // Vue.js 单文件组件
|
||||||
|
'.jsx', // React JSX 文件
|
||||||
|
'.tsx', // React TSX 文件
|
||||||
|
'.graphql', // GraphQL 查询语言文件
|
||||||
|
'.gql', // GraphQL 查询语言文件
|
||||||
|
'.proto', // Protocol Buffers 文件
|
||||||
|
'.thrift', // Thrift 文件
|
||||||
|
'.toml', // TOML 配置文件
|
||||||
|
'.edn', // Clojure 数据表示文件
|
||||||
|
'.cake', // CakePHP 配置文件
|
||||||
|
'.ctp', // CakePHP 视图文件
|
||||||
|
'.cfm', // ColdFusion 标记语言文件
|
||||||
|
'.cfc', // ColdFusion 组件文件
|
||||||
|
'.m', // Objective-C 源文件
|
||||||
|
'.mm', // Objective-C++ 源文件
|
||||||
|
'.gradle', // Gradle 构建文件
|
||||||
|
'.groovy', // Gradle 构建文件
|
||||||
|
'.gradle', // Gradle 构建文件
|
||||||
|
'.kts' // Kotlin Script 文件
|
||||||
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
export { default as UserAvatar } from '@renderer/assets/images/avatar.png'
|
||||||
export { default as AppLogo } from '@renderer/assets/images/logo.png'
|
export { default as AppLogo } from '@renderer/assets/images/logo.png'
|
||||||
|
|
||||||
export const APP_NAME = 'Cherry Studio'
|
export const APP_NAME = 'Cherry Studio'
|
||||||
|
|
||||||
export const isLocalAi = false
|
export const isLocalAi = false
|
||||||
|
|||||||
103
src/renderer/src/config/minapp.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import AiAssistantAppLogo from '@renderer/assets/images/apps/360-ai.png'
|
||||||
|
import AiSearchAppLogo from '@renderer/assets/images/apps/ai-search.png'
|
||||||
|
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png'
|
||||||
|
import DevvAppLogo from '@renderer/assets/images/apps/devv.png'
|
||||||
|
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
|
||||||
|
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp'
|
||||||
|
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp'
|
||||||
|
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
|
||||||
|
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png'
|
||||||
|
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png'
|
||||||
|
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png'
|
||||||
|
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png'
|
||||||
|
import MinApp from '@renderer/components/MinApp'
|
||||||
|
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
||||||
|
import { MinAppType } from '@renderer/types'
|
||||||
|
|
||||||
|
const _apps: MinAppType[] = [
|
||||||
|
{
|
||||||
|
id: '360-ai-so',
|
||||||
|
name: '360AI搜索',
|
||||||
|
logo: AiSearchAppLogo,
|
||||||
|
url: 'https://so.360.com/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '360-ai-bot',
|
||||||
|
name: 'AI 助手',
|
||||||
|
logo: AiAssistantAppLogo,
|
||||||
|
url: 'https://bot.360.com/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'baidu-ai-chat',
|
||||||
|
name: '文心一言',
|
||||||
|
logo: BaiduAiAppLogo,
|
||||||
|
url: 'https://yiyan.baidu.com/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tencent-yuanbao',
|
||||||
|
name: '腾讯元宝',
|
||||||
|
logo: TencentYuanbaoAppLogo,
|
||||||
|
url: 'https://yuanbao.tencent.com/chat'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sensetime-chat',
|
||||||
|
name: '商量',
|
||||||
|
logo: SensetimeAppLogo,
|
||||||
|
url: 'https://chat.sensetime.com/wb/chat'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'spark-desk',
|
||||||
|
name: 'SparkDesk',
|
||||||
|
logo: SparkDeskAppLogo,
|
||||||
|
url: 'https://xinghuo.xfyun.cn/desk'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'metaso',
|
||||||
|
name: '秘塔AI搜索',
|
||||||
|
logo: MetasoAppLogo,
|
||||||
|
url: 'https://metaso.cn/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'poe',
|
||||||
|
name: 'Poe',
|
||||||
|
logo: PoeAppLogo,
|
||||||
|
url: 'https://poe.com'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'perplexity',
|
||||||
|
name: 'perplexity',
|
||||||
|
logo: PerplexityAppLogo,
|
||||||
|
url: 'https://www.perplexity.ai/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'devv',
|
||||||
|
name: 'DEVV_',
|
||||||
|
logo: DevvAppLogo,
|
||||||
|
url: 'https://devv.ai/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tiangong-ai',
|
||||||
|
name: '天工AI',
|
||||||
|
logo: TiangongAiLogo,
|
||||||
|
url: 'https://www.tiangong.cn/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'zhihu-zhiada',
|
||||||
|
name: '知乎直答',
|
||||||
|
logo: ZhihuAppLogo,
|
||||||
|
url: 'https://zhida.zhihu.com/'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export function getAllMinApps() {
|
||||||
|
const list: MinAppType[] = (Object.entries(PROVIDER_CONFIG) as any[])
|
||||||
|
.filter(([, config]) => config.app)
|
||||||
|
.map(([key, config]) => ({ id: key, ...config.app }))
|
||||||
|
.concat(_apps)
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startMinAppById(id: string) {
|
||||||
|
const app = getAllMinApps().find((app) => app?.id === id)
|
||||||
|
app && MinApp.start(app)
|
||||||
|
}
|
||||||
@@ -1,37 +1,67 @@
|
|||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
|
|
||||||
type SystemModel = Model & { enabled: boolean }
|
const TEXT_TO_IMAGE_REGEX = /flux|diffusion|stabilityai|sd-turbo|dall|cogview/i
|
||||||
|
const VISION_REGEX = /llava|moondream|minicpm|gemini-1.5|claude-3|vision|glm-4v|gpt-4|qwen-vl/i
|
||||||
|
const EMBEDDING_REGEX = /embedding/i
|
||||||
|
|
||||||
export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||||
ollama: [],
|
ollama: [],
|
||||||
|
silicon: [
|
||||||
|
{
|
||||||
|
id: 'Qwen/Qwen2-7B-Instruct',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'Qwen2-7B-Instruct',
|
||||||
|
group: 'Qwen2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Qwen/Qwen2-72B-Instruct',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'Qwen2-72B-Instruct',
|
||||||
|
group: 'Qwen2'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'THUDM/glm-4-9b-chat',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'GLM-4-9B-Chat',
|
||||||
|
group: 'GLM'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deepseek-ai/DeepSeek-V2-Chat',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'DeepSeek-V2-Chat',
|
||||||
|
group: 'DeepSeek'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deepseek-ai/DeepSeek-Coder-V2-Instruct',
|
||||||
|
provider: 'silicon',
|
||||||
|
name: 'DeepSeek-Coder-V2-Instruct',
|
||||||
|
group: 'DeepSeek'
|
||||||
|
}
|
||||||
|
],
|
||||||
openai: [
|
openai: [
|
||||||
{
|
{
|
||||||
id: 'gpt-4o',
|
id: 'gpt-4o',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
name: ' GPT-4o',
|
name: ' GPT-4o',
|
||||||
group: 'GPT 4o',
|
group: 'GPT 4o'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gpt-4o-mini',
|
id: 'gpt-4o-mini',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
name: ' GPT-4o-mini',
|
name: ' GPT-4o-mini',
|
||||||
group: 'GPT 4o',
|
group: 'GPT 4o'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gpt-4-turbo',
|
id: 'gpt-4-turbo',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
name: ' GPT-4 Turbo',
|
name: ' GPT-4 Turbo',
|
||||||
group: 'GPT 4',
|
group: 'GPT 4'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gpt-4',
|
id: 'gpt-4',
|
||||||
provider: 'openai',
|
provider: 'openai',
|
||||||
name: ' GPT-4',
|
name: ' GPT-4',
|
||||||
group: 'GPT 4',
|
group: 'GPT 4'
|
||||||
enabled: true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
gemini: [
|
gemini: [
|
||||||
@@ -39,15 +69,13 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'gemini-1.5-flash',
|
id: 'gemini-1.5-flash',
|
||||||
provider: 'gemini',
|
provider: 'gemini',
|
||||||
name: 'Gemini 1.5 Flash',
|
name: 'Gemini 1.5 Flash',
|
||||||
group: 'Gemini 1.5',
|
group: 'Gemini 1.5'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gemini-1.5-pro-exp-0801',
|
id: 'gemini-1.5-pro-exp-0801',
|
||||||
provider: 'gemini',
|
provider: 'gemini',
|
||||||
name: 'Gemini 1.5 Pro Experimental 0801',
|
name: 'Gemini 1.5 Pro Experimental 0801',
|
||||||
group: 'Gemini 1.5',
|
group: 'Gemini 1.5'
|
||||||
enabled: true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
anthropic: [
|
anthropic: [
|
||||||
@@ -55,143 +83,25 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'claude-3-5-sonnet-20240620',
|
id: 'claude-3-5-sonnet-20240620',
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
name: 'Claude 3.5 Sonnet',
|
name: 'Claude 3.5 Sonnet',
|
||||||
group: 'Claude 3.5',
|
group: 'Claude 3.5'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'claude-3-opus-20240229',
|
id: 'claude-3-opus-20240229',
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
name: 'Claude 3 Opus',
|
name: 'Claude 3 Opus',
|
||||||
group: 'Claude 3',
|
group: 'Claude 3'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'claude-3-sonnet-20240229',
|
id: 'claude-3-sonnet-20240229',
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
name: 'Claude 3 Sonnet',
|
name: 'Claude 3 Sonnet',
|
||||||
group: 'Claude 3',
|
group: 'Claude 3'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'claude-3-haiku-20240307',
|
id: 'claude-3-haiku-20240307',
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
name: 'Claude 3 Haiku',
|
name: 'Claude 3 Haiku',
|
||||||
group: 'Claude 3',
|
group: 'Claude 3'
|
||||||
enabled: true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
silicon: [
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen2-7B-Instruct',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen2-7B-Instruct',
|
|
||||||
group: 'Qwen2',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen2-1.5B-Instruct',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen2-1.5B-Instruct',
|
|
||||||
group: 'Qwen2',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen1.5-7B-Chat',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen1.5-7B-Chat',
|
|
||||||
group: 'Qwen1.5',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen2-72B-Instruct',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen2-72B-Instruct',
|
|
||||||
group: 'Qwen2',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen2-57B-A14B-Instruct',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen2-57B-A14B-Instruct',
|
|
||||||
group: 'Qwen2',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen1.5-110B-Chat',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen1.5-110B-Chat',
|
|
||||||
group: 'Qwen1.5',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen1.5-32B-Chat',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen1.5-32B-Chat',
|
|
||||||
group: 'Qwen1.5',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Qwen/Qwen1.5-14B-Chat',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Qwen1.5-14B-Chat',
|
|
||||||
group: 'Qwen1.5',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'deepseek-ai/DeepSeek-V2-Chat',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'DeepSeek-V2-Chat',
|
|
||||||
group: 'DeepSeek',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'deepseek-ai/DeepSeek-Coder-V2-Instruct',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'DeepSeek-Coder-V2-Instruct',
|
|
||||||
group: 'DeepSeek',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'deepseek-ai/deepseek-llm-67b-chat',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Deepseek-LLM-67B-Chat',
|
|
||||||
group: 'DeepSeek',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'THUDM/glm-4-9b-chat',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'GLM-4-9B-Chat',
|
|
||||||
group: 'GLM',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'THUDM/chatglm3-6b',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'GhatGLM3-6B',
|
|
||||||
group: 'GLM',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '01-ai/Yi-1.5-9B-Chat-16K',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Yi-1.5-9B-Chat-16K',
|
|
||||||
group: 'Yi',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '01-ai/Yi-1.5-6B-Chat',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Yi-1.5-6B-Chat',
|
|
||||||
group: 'Yi',
|
|
||||||
enabled: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '01-ai/Yi-1.5-34B-Chat-16K',
|
|
||||||
provider: 'silicon',
|
|
||||||
name: 'Yi-1.5-34B-Chat-16K',
|
|
||||||
group: 'Yi',
|
|
||||||
enabled: false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
deepseek: [
|
deepseek: [
|
||||||
@@ -199,15 +109,21 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'deepseek-chat',
|
id: 'deepseek-chat',
|
||||||
provider: 'deepseek',
|
provider: 'deepseek',
|
||||||
name: 'DeepSeek Chat',
|
name: 'DeepSeek Chat',
|
||||||
group: 'DeepSeek Chat',
|
group: 'DeepSeek Chat'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'deepseek-coder',
|
id: 'deepseek-coder',
|
||||||
provider: 'deepseek',
|
provider: 'deepseek',
|
||||||
name: 'DeepSeek Coder',
|
name: 'DeepSeek Coder',
|
||||||
group: 'DeepSeek Coder',
|
group: 'DeepSeek Coder'
|
||||||
enabled: true
|
}
|
||||||
|
],
|
||||||
|
github: [
|
||||||
|
{
|
||||||
|
id: 'gpt-4o',
|
||||||
|
provider: 'github',
|
||||||
|
name: 'OpenAI GPT-4o',
|
||||||
|
group: 'OpenAI'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
yi: [
|
yi: [
|
||||||
@@ -215,87 +131,87 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'yi-large',
|
id: 'yi-large',
|
||||||
provider: 'yi',
|
provider: 'yi',
|
||||||
name: 'Yi-Large',
|
name: 'Yi-Large',
|
||||||
group: 'Yi',
|
group: 'Yi'
|
||||||
enabled: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'yi-large-turbo',
|
id: 'yi-large-turbo',
|
||||||
provider: 'yi',
|
provider: 'yi',
|
||||||
name: 'Yi-Large-Turbo',
|
name: 'Yi-Large-Turbo',
|
||||||
group: 'Yi',
|
group: 'Yi'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'yi-large-rag',
|
id: 'yi-large-rag',
|
||||||
provider: 'yi',
|
provider: 'yi',
|
||||||
name: 'Yi-Large-Rag',
|
name: 'Yi-Large-Rag',
|
||||||
group: 'Yi',
|
group: 'Yi'
|
||||||
enabled: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'yi-medium',
|
id: 'yi-medium',
|
||||||
provider: 'yi',
|
provider: 'yi',
|
||||||
name: 'Yi-Medium',
|
name: 'Yi-Medium',
|
||||||
group: 'Yi',
|
group: 'Yi'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'yi-medium-200k',
|
id: 'yi-medium-200k',
|
||||||
provider: 'yi',
|
provider: 'yi',
|
||||||
name: 'Yi-Medium-200k',
|
name: 'Yi-Medium-200k',
|
||||||
group: 'Yi',
|
group: 'Yi'
|
||||||
enabled: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'yi-spark',
|
id: 'yi-spark',
|
||||||
provider: 'yi',
|
provider: 'yi',
|
||||||
name: 'Yi-Spark',
|
name: 'Yi-Spark',
|
||||||
group: 'Yi',
|
group: 'Yi'
|
||||||
enabled: false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
zhipu: [
|
zhipu: [
|
||||||
{
|
|
||||||
id: 'glm-4-0520',
|
|
||||||
provider: 'zhipu',
|
|
||||||
name: 'GLM-4-0520',
|
|
||||||
group: 'GLM',
|
|
||||||
enabled: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'glm-4',
|
id: 'glm-4',
|
||||||
provider: 'zhipu',
|
provider: 'zhipu',
|
||||||
name: 'GLM-4',
|
name: 'GLM-4',
|
||||||
group: 'GLM',
|
group: 'GLM-4'
|
||||||
enabled: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'glm-4-airx',
|
id: 'glm-4-plus',
|
||||||
provider: 'zhipu',
|
provider: 'zhipu',
|
||||||
name: 'GLM-4-AirX',
|
name: 'GLM-4-Plus',
|
||||||
group: 'GLM',
|
group: 'GLM-4'
|
||||||
enabled: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'glm-4-air',
|
id: 'glm-4-air',
|
||||||
provider: 'zhipu',
|
provider: 'zhipu',
|
||||||
name: 'GLM-4-Air',
|
name: 'GLM-4-Air',
|
||||||
group: 'GLM',
|
group: 'GLM-4'
|
||||||
enabled: true
|
},
|
||||||
|
{
|
||||||
|
id: 'glm-4-airx',
|
||||||
|
provider: 'zhipu',
|
||||||
|
name: 'GLM-4-AirX',
|
||||||
|
group: 'GLM-4'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'glm-4-flash',
|
||||||
|
provider: 'zhipu',
|
||||||
|
name: 'GLM-4-Flash',
|
||||||
|
group: 'GLM-4'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'glm-4v',
|
id: 'glm-4v',
|
||||||
provider: 'zhipu',
|
provider: 'zhipu',
|
||||||
name: 'GLM-4V',
|
name: 'GLM 4V',
|
||||||
group: 'GLM',
|
group: 'GLM-4v'
|
||||||
enabled: false
|
},
|
||||||
|
{
|
||||||
|
id: 'glm-4v-plus',
|
||||||
|
provider: 'zhipu',
|
||||||
|
name: 'GLM-4V-Plus',
|
||||||
|
group: 'GLM-4v'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'glm-4-alltools',
|
id: 'glm-4-alltools',
|
||||||
provider: 'zhipu',
|
provider: 'zhipu',
|
||||||
name: 'GLM-4-AllTools',
|
name: 'GLM-4-AllTools',
|
||||||
group: 'GLM',
|
group: 'GLM-4-AllTools'
|
||||||
enabled: false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
moonshot: [
|
moonshot: [
|
||||||
@@ -303,22 +219,19 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'moonshot-v1-8k',
|
id: 'moonshot-v1-8k',
|
||||||
provider: 'moonshot',
|
provider: 'moonshot',
|
||||||
name: 'Moonshot V1 8k',
|
name: 'Moonshot V1 8k',
|
||||||
group: 'Moonshot V1',
|
group: 'Moonshot V1'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'moonshot-v1-32k',
|
id: 'moonshot-v1-32k',
|
||||||
provider: 'moonshot',
|
provider: 'moonshot',
|
||||||
name: 'Moonshot V1 32k',
|
name: 'Moonshot V1 32k',
|
||||||
group: 'Moonshot V1',
|
group: 'Moonshot V1'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'moonshot-v1-128k',
|
id: 'moonshot-v1-128k',
|
||||||
provider: 'moonshot',
|
provider: 'moonshot',
|
||||||
name: 'Moonshot V1 128k',
|
name: 'Moonshot V1 128k',
|
||||||
group: 'Moonshot V1',
|
group: 'Moonshot V1'
|
||||||
enabled: true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
baichuan: [
|
baichuan: [
|
||||||
@@ -326,22 +239,19 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'Baichuan4',
|
id: 'Baichuan4',
|
||||||
provider: 'baichuan',
|
provider: 'baichuan',
|
||||||
name: 'Baichuan4',
|
name: 'Baichuan4',
|
||||||
group: 'Baichuan4',
|
group: 'Baichuan4'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Baichuan3-Turbo',
|
id: 'Baichuan3-Turbo',
|
||||||
provider: 'baichuan',
|
provider: 'baichuan',
|
||||||
name: 'Baichuan3 Turbo',
|
name: 'Baichuan3 Turbo',
|
||||||
group: 'Baichuan3',
|
group: 'Baichuan3'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Baichuan3-Turbo-128k',
|
id: 'Baichuan3-Turbo-128k',
|
||||||
provider: 'baichuan',
|
provider: 'baichuan',
|
||||||
name: 'Baichuan3 Turbo 128k',
|
name: 'Baichuan3 Turbo 128k',
|
||||||
group: 'Baichuan3',
|
group: 'Baichuan3'
|
||||||
enabled: true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
dashscope: [
|
dashscope: [
|
||||||
@@ -349,22 +259,19 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'qwen-turbo',
|
id: 'qwen-turbo',
|
||||||
provider: 'dashscope',
|
provider: 'dashscope',
|
||||||
name: 'Qwen Turbo',
|
name: 'Qwen Turbo',
|
||||||
group: 'Qwen',
|
group: 'Qwen'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'qwen-plus',
|
id: 'qwen-plus',
|
||||||
provider: 'dashscope',
|
provider: 'dashscope',
|
||||||
name: 'Qwen Plus',
|
name: 'Qwen Plus',
|
||||||
group: 'Qwen',
|
group: 'Qwen'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'qwen-max',
|
id: 'qwen-max',
|
||||||
provider: 'dashscope',
|
provider: 'dashscope',
|
||||||
name: 'Qwen Max',
|
name: 'Qwen Max',
|
||||||
group: 'Qwen',
|
group: 'Qwen'
|
||||||
enabled: true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
stepfun: [
|
stepfun: [
|
||||||
@@ -372,15 +279,13 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'step-1-8k',
|
id: 'step-1-8k',
|
||||||
provider: 'stepfun',
|
provider: 'stepfun',
|
||||||
name: 'Step 1 8K',
|
name: 'Step 1 8K',
|
||||||
group: 'Step 1',
|
group: 'Step 1'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'step-1-flash',
|
id: 'step-1-flash',
|
||||||
provider: 'stepfun',
|
provider: 'stepfun',
|
||||||
name: 'Step 1 Flash',
|
name: 'Step 1 Flash',
|
||||||
group: 'Step 1',
|
group: 'Step 1'
|
||||||
enabled: true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
doubao: [],
|
doubao: [],
|
||||||
@@ -389,29 +294,25 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'abab6.5s-chat',
|
id: 'abab6.5s-chat',
|
||||||
provider: 'minimax',
|
provider: 'minimax',
|
||||||
name: 'abab6.5s',
|
name: 'abab6.5s',
|
||||||
group: 'abab6',
|
group: 'abab6'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'abab6.5g-chat',
|
id: 'abab6.5g-chat',
|
||||||
provider: 'minimax',
|
provider: 'minimax',
|
||||||
name: 'abab6.5g',
|
name: 'abab6.5g',
|
||||||
group: 'abab6',
|
group: 'abab6'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'abab6.5t-chat',
|
id: 'abab6.5t-chat',
|
||||||
provider: 'minimax',
|
provider: 'minimax',
|
||||||
name: 'abab6.5t',
|
name: 'abab6.5t',
|
||||||
group: 'abab6',
|
group: 'abab6'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'abab5.5s-chat',
|
id: 'abab5.5s-chat',
|
||||||
provider: 'minimax',
|
provider: 'minimax',
|
||||||
name: 'abab5.5s',
|
name: 'abab5.5s',
|
||||||
group: 'abab5',
|
group: 'abab5'
|
||||||
enabled: true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
aihubmix: [
|
aihubmix: [
|
||||||
@@ -419,15 +320,13 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'gpt-4o-mini',
|
id: 'gpt-4o-mini',
|
||||||
provider: 'aihubmix',
|
provider: 'aihubmix',
|
||||||
name: 'GPT-4o Mini',
|
name: 'GPT-4o Mini',
|
||||||
group: 'GPT-4o',
|
group: 'GPT-4o'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'aihubmix-Llama-3-70B-Instruct',
|
id: 'aihubmix-Llama-3-70B-Instruct',
|
||||||
provider: 'aihubmix',
|
provider: 'aihubmix',
|
||||||
name: 'Llama 3 70B Instruct',
|
name: 'Llama 3 70B Instruct',
|
||||||
group: 'Llama3',
|
group: 'Llama3'
|
||||||
enabled: true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
openrouter: [
|
openrouter: [
|
||||||
@@ -435,36 +334,31 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'google/gemma-2-9b-it:free',
|
id: 'google/gemma-2-9b-it:free',
|
||||||
provider: 'openrouter',
|
provider: 'openrouter',
|
||||||
name: 'Google: Gemma 2 9B',
|
name: 'Google: Gemma 2 9B',
|
||||||
group: 'Gemma',
|
group: 'Gemma'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'microsoft/phi-3-mini-128k-instruct:free',
|
id: 'microsoft/phi-3-mini-128k-instruct:free',
|
||||||
provider: 'openrouter',
|
provider: 'openrouter',
|
||||||
name: 'Phi-3 Mini 128K Instruct',
|
name: 'Phi-3 Mini 128K Instruct',
|
||||||
group: 'Phi',
|
group: 'Phi'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'microsoft/phi-3-medium-128k-instruct:free',
|
id: 'microsoft/phi-3-medium-128k-instruct:free',
|
||||||
provider: 'openrouter',
|
provider: 'openrouter',
|
||||||
name: 'Phi-3 Medium 128K Instruct',
|
name: 'Phi-3 Medium 128K Instruct',
|
||||||
group: 'Phi',
|
group: 'Phi'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'meta-llama/llama-3-8b-instruct:free',
|
id: 'meta-llama/llama-3-8b-instruct:free',
|
||||||
provider: 'openrouter',
|
provider: 'openrouter',
|
||||||
name: 'Meta: Llama 3 8B Instruct',
|
name: 'Meta: Llama 3 8B Instruct',
|
||||||
group: 'Llama3',
|
group: 'Llama3'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'mistralai/mistral-7b-instruct:free',
|
id: 'mistralai/mistral-7b-instruct:free',
|
||||||
provider: 'openrouter',
|
provider: 'openrouter',
|
||||||
name: 'Mistral: Mistral 7B Instruct',
|
name: 'Mistral: Mistral 7B Instruct',
|
||||||
group: 'Mistral',
|
group: 'Mistral'
|
||||||
enabled: true
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
groq: [
|
groq: [
|
||||||
@@ -472,29 +366,37 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
|
|||||||
id: 'llama3-8b-8192',
|
id: 'llama3-8b-8192',
|
||||||
provider: 'groq',
|
provider: 'groq',
|
||||||
name: 'LLaMA3 8B',
|
name: 'LLaMA3 8B',
|
||||||
group: 'Llama3',
|
group: 'Llama3'
|
||||||
enabled: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'llama3-70b-8192',
|
id: 'llama3-70b-8192',
|
||||||
provider: 'groq',
|
provider: 'groq',
|
||||||
name: 'LLaMA3 70B',
|
name: 'LLaMA3 70B',
|
||||||
group: 'Llama3',
|
group: 'Llama3'
|
||||||
enabled: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'mixtral-8x7b-32768',
|
id: 'mixtral-8x7b-32768',
|
||||||
provider: 'groq',
|
provider: 'groq',
|
||||||
name: 'Mixtral 8x7B',
|
name: 'Mixtral 8x7B',
|
||||||
group: 'Mixtral',
|
group: 'Mixtral'
|
||||||
enabled: false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gemma-7b-it',
|
id: 'gemma-7b-it',
|
||||||
provider: 'groq',
|
provider: 'groq',
|
||||||
name: 'Gemma 7B',
|
name: 'Gemma 7B',
|
||||||
group: 'Gemma',
|
group: 'Gemma'
|
||||||
enabled: false
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isTextToImageModel(model: Model): boolean {
|
||||||
|
return TEXT_TO_IMAGE_REGEX.test(model.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEmbeddingModel(model: Model): boolean {
|
||||||
|
return EMBEDDING_REGEX.test(model.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isVisionModel(model: Model): boolean {
|
||||||
|
return VISION_REGEX.test(model.id)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp'
|
|||||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
|
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
|
||||||
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png'
|
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png'
|
||||||
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
|
import BaichuanModelLogo from '@renderer/assets/images/models/baichuan.png'
|
||||||
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.jpeg'
|
import ChatGLMModelLogo from '@renderer/assets/images/models/chatglm.png'
|
||||||
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
|
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
|
||||||
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
|
import ClaudeModelLogo from '@renderer/assets/images/models/claude.png'
|
||||||
|
import CohereModelLogo from '@renderer/assets/images/models/cohere.webp'
|
||||||
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
|
import DeepSeekModelLogo from '@renderer/assets/images/models/deepseek.png'
|
||||||
import DoubaoModelLogo from '@renderer/assets/images/models/doubao.png'
|
import DoubaoModelLogo from '@renderer/assets/images/models/doubao.png'
|
||||||
import EmbeddingModelLogo from '@renderer/assets/images/models/embedding.png'
|
import EmbeddingModelLogo from '@renderer/assets/images/models/embedding.png'
|
||||||
@@ -13,6 +14,7 @@ import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
|
|||||||
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png'
|
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png'
|
||||||
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
|
import LlamaModelLogo from '@renderer/assets/images/models/llama.jpeg'
|
||||||
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
|
import MicrosoftModelLogo from '@renderer/assets/images/models/microsoft.png'
|
||||||
|
import MinicpmModelLogo from '@renderer/assets/images/models/minicpm.webp'
|
||||||
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
|
import MixtralModelLogo from '@renderer/assets/images/models/mixtral.jpeg'
|
||||||
import PalmModelLogo from '@renderer/assets/images/models/palm.svg'
|
import PalmModelLogo from '@renderer/assets/images/models/palm.svg'
|
||||||
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
|
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
|
||||||
@@ -25,6 +27,7 @@ import DashScopeProviderLogo from '@renderer/assets/images/providers/dashscope.p
|
|||||||
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
|
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
|
||||||
import DoubaoProviderLogo from '@renderer/assets/images/providers/doubao.png'
|
import DoubaoProviderLogo from '@renderer/assets/images/providers/doubao.png'
|
||||||
import GeminiProviderLogo from '@renderer/assets/images/providers/gemini.png'
|
import GeminiProviderLogo from '@renderer/assets/images/providers/gemini.png'
|
||||||
|
import GithubProviderLogo from '@renderer/assets/images/providers/github.svg'
|
||||||
import GraphRagProviderLogo from '@renderer/assets/images/providers/graph-rag.png'
|
import GraphRagProviderLogo from '@renderer/assets/images/providers/graph-rag.png'
|
||||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
||||||
import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png'
|
import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png'
|
||||||
@@ -76,6 +79,8 @@ export function getProviderLogo(providerId: string) {
|
|||||||
return GraphRagProviderLogo
|
return GraphRagProviderLogo
|
||||||
case 'minimax':
|
case 'minimax':
|
||||||
return MinimaxProviderLogo
|
return MinimaxProviderLogo
|
||||||
|
case 'github':
|
||||||
|
return GithubProviderLogo
|
||||||
default:
|
default:
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@@ -87,6 +92,7 @@ export function getModelLogo(modelId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const logoMap = {
|
const logoMap = {
|
||||||
|
o1: OpenAiProviderLogo,
|
||||||
gpt: ChatGPTModelLogo,
|
gpt: ChatGPTModelLogo,
|
||||||
glm: ChatGLMModelLogo,
|
glm: ChatGLMModelLogo,
|
||||||
deepseek: DeepSeekModelLogo,
|
deepseek: DeepSeekModelLogo,
|
||||||
@@ -106,7 +112,10 @@ export function getModelLogo(modelId: string) {
|
|||||||
palm: PalmModelLogo,
|
palm: PalmModelLogo,
|
||||||
step: StepModelLogo,
|
step: StepModelLogo,
|
||||||
abab: HailuoModelLogo,
|
abab: HailuoModelLogo,
|
||||||
'ep-202': DoubaoModelLogo
|
'ep-202': DoubaoModelLogo,
|
||||||
|
cohere: CohereModelLogo,
|
||||||
|
command: CohereModelLogo,
|
||||||
|
minicpm: MinicpmModelLogo
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in logoMap) {
|
for (const key in logoMap) {
|
||||||
@@ -121,8 +130,7 @@ export function getModelLogo(modelId: string) {
|
|||||||
export const PROVIDER_CONFIG = {
|
export const PROVIDER_CONFIG = {
|
||||||
openai: {
|
openai: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.openai.com',
|
url: 'https://api.openai.com'
|
||||||
editable: true
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://openai.com/',
|
official: 'https://openai.com/',
|
||||||
@@ -131,6 +139,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://platform.openai.com/docs/models'
|
models: 'https://platform.openai.com/docs/models'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'openai',
|
||||||
name: 'ChatGPT',
|
name: 'ChatGPT',
|
||||||
url: 'https://chatgpt.com/',
|
url: 'https://chatgpt.com/',
|
||||||
logo: OpenAiProviderLogo
|
logo: OpenAiProviderLogo
|
||||||
@@ -138,8 +147,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
gemini: {
|
gemini: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://generativelanguage.googleapis.com',
|
url: 'https://generativelanguage.googleapis.com'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://gemini.google.com/',
|
official: 'https://gemini.google.com/',
|
||||||
@@ -148,6 +156,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://ai.google.dev/gemini-api/docs/models/gemini'
|
models: 'https://ai.google.dev/gemini-api/docs/models/gemini'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'gemini',
|
||||||
name: 'Gemini',
|
name: 'Gemini',
|
||||||
url: 'https://gemini.google.com/',
|
url: 'https://gemini.google.com/',
|
||||||
logo: GeminiProviderLogo
|
logo: GeminiProviderLogo
|
||||||
@@ -155,8 +164,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
silicon: {
|
silicon: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://cloud.siliconflow.cn',
|
url: 'https://cloud.siliconflow.cn'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://www.siliconflow.cn/',
|
official: 'https://www.siliconflow.cn/',
|
||||||
@@ -165,6 +173,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://docs.siliconflow.cn/docs/model-names'
|
models: 'https://docs.siliconflow.cn/docs/model-names'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'silicon',
|
||||||
name: 'SiliconFlow',
|
name: 'SiliconFlow',
|
||||||
url: 'https://cloud.siliconflow.cn/playground/chat',
|
url: 'https://cloud.siliconflow.cn/playground/chat',
|
||||||
logo: SiliconFlowProviderLogo
|
logo: SiliconFlowProviderLogo
|
||||||
@@ -172,8 +181,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
deepseek: {
|
deepseek: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.deepseek.com',
|
url: 'https://api.deepseek.com'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://deepseek.com/',
|
official: 'https://deepseek.com/',
|
||||||
@@ -182,15 +190,26 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://platform.deepseek.com/api-docs/'
|
models: 'https://platform.deepseek.com/api-docs/'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'deepseek',
|
||||||
name: 'DeepSeek',
|
name: 'DeepSeek',
|
||||||
url: 'https://chat.deepseek.com/',
|
url: 'https://chat.deepseek.com/',
|
||||||
logo: DeepSeekProviderLogo
|
logo: DeepSeekProviderLogo
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
github: {
|
||||||
|
api: {
|
||||||
|
url: 'https://models.inference.ai.azure.com/'
|
||||||
|
},
|
||||||
|
websites: {
|
||||||
|
official: 'https://github.com/marketplace/models',
|
||||||
|
apiKey: 'https://github.com/settings/tokens',
|
||||||
|
docs: 'https://docs.github.com/en/github-models',
|
||||||
|
models: 'https://github.com/marketplace/models'
|
||||||
|
}
|
||||||
|
},
|
||||||
yi: {
|
yi: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.lingyiwanwu.com',
|
url: 'https://api.lingyiwanwu.com'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://platform.lingyiwanwu.com/',
|
official: 'https://platform.lingyiwanwu.com/',
|
||||||
@@ -199,6 +218,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://platform.lingyiwanwu.com/docs#%E6%A8%A1%E5%9E%8B'
|
models: 'https://platform.lingyiwanwu.com/docs#%E6%A8%A1%E5%9E%8B'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'yi',
|
||||||
name: 'Yi',
|
name: 'Yi',
|
||||||
url: 'https://www.wanzhi.com/',
|
url: 'https://www.wanzhi.com/',
|
||||||
logo: YiProviderLogo
|
logo: YiProviderLogo
|
||||||
@@ -206,8 +226,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
zhipu: {
|
zhipu: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://open.bigmodel.cn/api/paas/v4/',
|
url: 'https://open.bigmodel.cn/api/paas/v4/'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://open.bigmodel.cn/',
|
official: 'https://open.bigmodel.cn/',
|
||||||
@@ -216,6 +235,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://open.bigmodel.cn/modelcenter/square'
|
models: 'https://open.bigmodel.cn/modelcenter/square'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'zhipu',
|
||||||
name: '智谱',
|
name: '智谱',
|
||||||
url: 'https://chatglm.cn/main/alltoolsdetail',
|
url: 'https://chatglm.cn/main/alltoolsdetail',
|
||||||
logo: ZhipuProviderLogo
|
logo: ZhipuProviderLogo
|
||||||
@@ -223,8 +243,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
moonshot: {
|
moonshot: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.moonshot.cn',
|
url: 'https://api.moonshot.cn'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://moonshot.ai/',
|
official: 'https://moonshot.ai/',
|
||||||
@@ -233,6 +252,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8'
|
models: 'https://platform.moonshot.cn/docs/intro#%E6%A8%A1%E5%9E%8B%E5%88%97%E8%A1%A8'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'moonshot',
|
||||||
name: 'Kimi',
|
name: 'Kimi',
|
||||||
url: 'https://kimi.moonshot.cn/',
|
url: 'https://kimi.moonshot.cn/',
|
||||||
logo: KimiAppLogo
|
logo: KimiAppLogo
|
||||||
@@ -240,8 +260,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
baichuan: {
|
baichuan: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.baichuan-ai.com',
|
url: 'https://api.baichuan-ai.com'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://www.baichuan-ai.com/',
|
official: 'https://www.baichuan-ai.com/',
|
||||||
@@ -250,6 +269,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://platform.baichuan-ai.com/price'
|
models: 'https://platform.baichuan-ai.com/price'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'baichuan',
|
||||||
name: '百小应',
|
name: '百小应',
|
||||||
url: 'https://ying.baichuan-ai.com/chat',
|
url: 'https://ying.baichuan-ai.com/chat',
|
||||||
logo: BaicuanAppLogo
|
logo: BaicuanAppLogo
|
||||||
@@ -257,8 +277,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
dashscope: {
|
dashscope: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/',
|
url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://dashscope.aliyun.com/',
|
official: 'https://dashscope.aliyun.com/',
|
||||||
@@ -267,6 +286,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://dashscope.console.aliyun.com/model'
|
models: 'https://dashscope.console.aliyun.com/model'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'dashscope',
|
||||||
name: '通义千问',
|
name: '通义千问',
|
||||||
url: 'https://tongyi.aliyun.com/qianwen/',
|
url: 'https://tongyi.aliyun.com/qianwen/',
|
||||||
logo: QwenModelLogo
|
logo: QwenModelLogo
|
||||||
@@ -274,8 +294,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
stepfun: {
|
stepfun: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.stepfun.com',
|
url: 'https://api.stepfun.com'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://platform.stepfun.com/',
|
official: 'https://platform.stepfun.com/',
|
||||||
@@ -284,6 +303,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://platform.stepfun.com/docs/llm/text'
|
models: 'https://platform.stepfun.com/docs/llm/text'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'stepfun',
|
||||||
name: '跃问',
|
name: '跃问',
|
||||||
url: 'https://yuewen.cn/chats/new',
|
url: 'https://yuewen.cn/chats/new',
|
||||||
logo: YuewenAppLogo
|
logo: YuewenAppLogo
|
||||||
@@ -291,8 +311,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
doubao: {
|
doubao: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://ark.cn-beijing.volces.com/api/v3/',
|
url: 'https://ark.cn-beijing.volces.com/api/v3/'
|
||||||
editable: true
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://console.volcengine.com/ark/',
|
official: 'https://console.volcengine.com/ark/',
|
||||||
@@ -301,6 +320,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint'
|
models: 'https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'doubao',
|
||||||
name: '豆包',
|
name: '豆包',
|
||||||
url: 'https://www.doubao.com/chat/',
|
url: 'https://www.doubao.com/chat/',
|
||||||
logo: DoubaoProviderLogo
|
logo: DoubaoProviderLogo
|
||||||
@@ -308,8 +328,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
minimax: {
|
minimax: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.minimax.chat/v1/',
|
url: 'https://api.minimax.chat/v1/'
|
||||||
editable: true
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://platform.minimaxi.com/',
|
official: 'https://platform.minimaxi.com/',
|
||||||
@@ -318,6 +337,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://platform.minimaxi.com/document/Models'
|
models: 'https://platform.minimaxi.com/document/Models'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'minimax',
|
||||||
name: '海螺',
|
name: '海螺',
|
||||||
url: 'https://hailuoai.com/',
|
url: 'https://hailuoai.com/',
|
||||||
logo: HailuoModelLogo
|
logo: HailuoModelLogo
|
||||||
@@ -325,14 +345,12 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
'graphrag-kylin-mountain': {
|
'graphrag-kylin-mountain': {
|
||||||
api: {
|
api: {
|
||||||
url: '',
|
url: ''
|
||||||
editable: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openrouter: {
|
openrouter: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://openrouter.ai/api/v1/',
|
url: 'https://openrouter.ai/api/v1/'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://openrouter.ai/',
|
official: 'https://openrouter.ai/',
|
||||||
@@ -343,8 +361,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
groq: {
|
groq: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.groq.com/openai',
|
url: 'https://api.groq.com/openai'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://groq.com/',
|
official: 'https://groq.com/',
|
||||||
@@ -353,6 +370,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://console.groq.com/docs/models'
|
models: 'https://console.groq.com/docs/models'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'groq',
|
||||||
name: 'Groq',
|
name: 'Groq',
|
||||||
url: 'https://chat.groq.com/',
|
url: 'https://chat.groq.com/',
|
||||||
logo: GroqProviderLogo
|
logo: GroqProviderLogo
|
||||||
@@ -360,8 +378,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
ollama: {
|
ollama: {
|
||||||
api: {
|
api: {
|
||||||
url: 'http://localhost:11434/v1/',
|
url: 'http://localhost:11434/v1/'
|
||||||
editable: true
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://ollama.com/',
|
official: 'https://ollama.com/',
|
||||||
@@ -371,8 +388,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
anthropic: {
|
anthropic: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://api.anthropic.com/',
|
url: 'https://api.anthropic.com/'
|
||||||
editable: true
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://anthropic.com/',
|
official: 'https://anthropic.com/',
|
||||||
@@ -381,6 +397,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
models: 'https://docs.anthropic.com/en/docs/about-claude/models'
|
models: 'https://docs.anthropic.com/en/docs/about-claude/models'
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
|
id: 'anthropic',
|
||||||
name: 'Claude',
|
name: 'Claude',
|
||||||
url: 'https://claude.ai/',
|
url: 'https://claude.ai/',
|
||||||
logo: AnthropicProviderLogo
|
logo: AnthropicProviderLogo
|
||||||
@@ -388,8 +405,7 @@ export const PROVIDER_CONFIG = {
|
|||||||
},
|
},
|
||||||
aihubmix: {
|
aihubmix: {
|
||||||
api: {
|
api: {
|
||||||
url: 'https://aihubmix.com',
|
url: 'https://aihubmix.com'
|
||||||
editable: false
|
|
||||||
},
|
},
|
||||||
websites: {
|
websites: {
|
||||||
official: 'https://aihubmix.com/',
|
official: 'https://aihubmix.com/',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { ThemeMode } from '@renderer/store/settings'
|
import { ThemeMode } from '@renderer/types'
|
||||||
import React, { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
|
import React, { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
|
||||||
|
|
||||||
interface ThemeContextType {
|
interface ThemeContextType {
|
||||||
27
src/renderer/src/databases/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { FileType, Topic } from '@renderer/types'
|
||||||
|
import { Dexie, type EntityTable } from 'dexie'
|
||||||
|
|
||||||
|
import { populateTopics } from './populate'
|
||||||
|
|
||||||
|
// Database declaration (move this to its own module also)
|
||||||
|
export const db = new Dexie('CherryStudio') as Dexie & {
|
||||||
|
files: EntityTable<FileType, 'id'>
|
||||||
|
topics: EntityTable<Pick<Topic, 'id' | 'messages'>, 'id'>
|
||||||
|
settings: EntityTable<{ id: string; value: any }, 'id'>
|
||||||
|
}
|
||||||
|
|
||||||
|
db.version(1).stores({
|
||||||
|
files: 'id, name, origin_name, path, size, ext, type, created_at, count'
|
||||||
|
})
|
||||||
|
|
||||||
|
db.version(2)
|
||||||
|
.stores({
|
||||||
|
files: 'id, name, origin_name, path, size, ext, type, created_at, count',
|
||||||
|
topics: '&id, messages',
|
||||||
|
settings: '&id, value'
|
||||||
|
})
|
||||||
|
.upgrade(populateTopics)
|
||||||
|
|
||||||
|
db.on('populate', populateTopics)
|
||||||
|
|
||||||
|
export default db
|
||||||
27
src/renderer/src/databases/populate.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import i18n from '@renderer/i18n'
|
||||||
|
import { Transaction } from 'dexie'
|
||||||
|
import localforage from 'localforage'
|
||||||
|
|
||||||
|
export async function populateTopics(trans: Transaction) {
|
||||||
|
const indexedKeys = await localforage.keys()
|
||||||
|
|
||||||
|
if (indexedKeys.length > 0) {
|
||||||
|
for (const key of indexedKeys) {
|
||||||
|
const value: any = await localforage.getItem(key)
|
||||||
|
if (key.startsWith('topic:')) {
|
||||||
|
await trans.db.table('topics').add({ id: value.id, messages: value.messages })
|
||||||
|
}
|
||||||
|
if (key === 'image://avatar') {
|
||||||
|
await trans.db.table('settings').add({ id: key, value: await localforage.getItem(key) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.modal.success({
|
||||||
|
title: i18n.t('message.upgrade.success.title'),
|
||||||
|
content: i18n.t('message.upgrade.success.content'),
|
||||||
|
okText: i18n.t('message.upgrade.success.button'),
|
||||||
|
centered: true,
|
||||||
|
onOk: () => window.api.reload()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { isLocalAi } from '@renderer/config/env'
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
|
import db from '@renderer/databases'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import LocalStorage from '@renderer/services/storage'
|
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setAvatar } from '@renderer/store/runtime'
|
import { setAvatar } from '@renderer/store/runtime'
|
||||||
import { runAsyncFunction } from '@renderer/utils'
|
import { runAsyncFunction } from '@renderer/utils'
|
||||||
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
import { useDefaultModel } from './useAssistant'
|
import { useDefaultModel } from './useAssistant'
|
||||||
@@ -11,16 +12,13 @@ import { useSettings } from './useSettings'
|
|||||||
|
|
||||||
export function useAppInit() {
|
export function useAppInit() {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { proxyUrl } = useSettings()
|
const { proxyUrl, language } = useSettings()
|
||||||
const { language } = useSettings()
|
|
||||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||||
|
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
avatar?.value && dispatch(setAvatar(avatar.value))
|
||||||
const storedImage = await LocalStorage.getImage('avatar')
|
}, [avatar, dispatch])
|
||||||
storedImage && dispatch(setAvatar(storedImage))
|
|
||||||
})
|
|
||||||
}, [dispatch])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ import {
|
|||||||
} from '@renderer/store/assistants'
|
} from '@renderer/store/assistants'
|
||||||
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
|
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
|
||||||
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
|
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
|
||||||
import localforage from 'localforage'
|
|
||||||
|
import { TopicManager } from './useTopic'
|
||||||
|
|
||||||
export function useAssistants() {
|
export function useAssistants() {
|
||||||
const { assistants } = useAppSelector((state) => state.assistants)
|
const { assistants } = useAppSelector((state) => state.assistants)
|
||||||
@@ -29,9 +30,8 @@ export function useAssistants() {
|
|||||||
removeAssistant: (id: string) => {
|
removeAssistant: (id: string) => {
|
||||||
dispatch(removeAssistant({ id }))
|
dispatch(removeAssistant({ id }))
|
||||||
const assistant = assistants.find((a) => a.id === id)
|
const assistant = assistants.find((a) => a.id === id)
|
||||||
if (assistant) {
|
const topics = assistant?.topics || []
|
||||||
assistant.topics.forEach((id) => localforage.removeItem(`topic:${id}`))
|
topics.forEach(({ id }) => TopicManager.removeTopic(id))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,14 @@ export function useAssistant(id: string) {
|
|||||||
assistant,
|
assistant,
|
||||||
model: assistant?.model ?? defaultModel,
|
model: assistant?.model ?? defaultModel,
|
||||||
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
|
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
|
||||||
removeTopic: (topic: Topic) => dispatch(removeTopic({ assistantId: assistant.id, topic })),
|
removeTopic: (topic: Topic) => {
|
||||||
|
TopicManager.removeTopic(topic.id)
|
||||||
|
dispatch(removeTopic({ assistantId: assistant.id, topic }))
|
||||||
|
},
|
||||||
|
moveTopic: (topic: Topic, toAssistant: Assistant) => {
|
||||||
|
dispatch(addTopic({ assistantId: toAssistant.id, topic: { ...topic } }))
|
||||||
|
dispatch(removeTopic({ assistantId: assistant.id, topic }))
|
||||||
|
},
|
||||||
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
|
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
|
||||||
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
|
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
|
||||||
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
|
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
|
||||||
|
|||||||
7
src/renderer/src/hooks/useModel.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { useProviders } from './useProvider'
|
||||||
|
|
||||||
|
export function useModel(id?: string) {
|
||||||
|
const { providers } = useProviders()
|
||||||
|
const allModels = providers.map((p) => p.models).flat()
|
||||||
|
return allModels.find((m) => m.id === id)
|
||||||
|
}
|
||||||
@@ -3,8 +3,10 @@ import {
|
|||||||
SendMessageShortcut,
|
SendMessageShortcut,
|
||||||
setSendMessageShortcut as _setSendMessageShortcut,
|
setSendMessageShortcut as _setSendMessageShortcut,
|
||||||
setTheme,
|
setTheme,
|
||||||
ThemeMode
|
setTopicPosition,
|
||||||
|
setWindowStyle
|
||||||
} from '@renderer/store/settings'
|
} from '@renderer/store/settings'
|
||||||
|
import { ThemeMode } from '@renderer/types'
|
||||||
|
|
||||||
export function useSettings() {
|
export function useSettings() {
|
||||||
const settings = useAppSelector((state) => state.settings)
|
const settings = useAppSelector((state) => state.settings)
|
||||||
@@ -17,6 +19,12 @@ export function useSettings() {
|
|||||||
},
|
},
|
||||||
setTheme(theme: ThemeMode) {
|
setTheme(theme: ThemeMode) {
|
||||||
dispatch(setTheme(theme))
|
dispatch(setTheme(theme))
|
||||||
|
},
|
||||||
|
setWindowStyle(windowStyle: 'transparent' | 'opaque') {
|
||||||
|
dispatch(setWindowStyle(windowStyle))
|
||||||
|
},
|
||||||
|
setTopicPosition(topicPosition: 'left' | 'right') {
|
||||||
|
dispatch(setTopicPosition(topicPosition))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setShowRightSidebar, toggleRightSidebar, toggleShowAssistants } from '@renderer/store/settings'
|
import { setShowTopics, toggleShowAssistants, toggleShowTopics } from '@renderer/store/settings'
|
||||||
|
|
||||||
export function useShowRightSidebar() {
|
|
||||||
const showRightSidebar = useAppSelector((state) => state.settings.showRightSidebar)
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
return {
|
|
||||||
rightSidebarShown: showRightSidebar,
|
|
||||||
toggleRightSidebar: () => dispatch(toggleRightSidebar()),
|
|
||||||
showRightSidebar: () => dispatch(setShowRightSidebar(true)),
|
|
||||||
hideRightSidebar: () => dispatch(setShowRightSidebar(false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useShowAssistants() {
|
export function useShowAssistants() {
|
||||||
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
|
const showAssistants = useAppSelector((state) => state.settings.showAssistants)
|
||||||
@@ -23,6 +11,17 @@ export function useShowAssistants() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useShowTopics() {
|
||||||
|
const showTopics = useAppSelector((state) => state.settings.showTopics)
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
return {
|
||||||
|
showTopics,
|
||||||
|
setShowTopics: (show: boolean) => dispatch(setShowTopics(show)),
|
||||||
|
toggleShowTopics: () => dispatch(toggleShowTopics())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function useRuntime() {
|
export function useRuntime() {
|
||||||
return useAppSelector((state) => state.runtime)
|
return useAppSelector((state) => state.runtime)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
|
import db from '@renderer/databases'
|
||||||
|
import { deleteMessageFiles } from '@renderer/services/messages'
|
||||||
import { Assistant, Topic } from '@renderer/types'
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
import { find } from 'lodash'
|
import { find } from 'lodash'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
import { useAssistant } from './useAssistant'
|
||||||
|
|
||||||
let _activeTopic: Topic
|
let _activeTopic: Topic
|
||||||
|
|
||||||
export function useActiveTopic(assistant: Assistant) {
|
export function useActiveTopic(_assistant: Assistant) {
|
||||||
|
const { assistant } = useAssistant(_assistant.id)
|
||||||
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
|
const [activeTopic, setActiveTopic] = useState(_activeTopic || assistant?.topics[0])
|
||||||
|
|
||||||
|
_activeTopic = activeTopic
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// activeTopic not in assistant.topics
|
// activeTopic not in assistant.topics
|
||||||
if (assistant && !find(assistant.topics, { id: activeTopic?.id })) {
|
if (assistant && !find(assistant.topics, { id: activeTopic?.id })) {
|
||||||
@@ -20,3 +27,38 @@ export function useActiveTopic(assistant: Assistant) {
|
|||||||
export function getTopic(assistant: Assistant, topicId: string) {
|
export function getTopic(assistant: Assistant, topicId: string) {
|
||||||
return assistant?.topics.find((topic) => topic.id === topicId)
|
return assistant?.topics.find((topic) => topic.id === topicId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class TopicManager {
|
||||||
|
static async getTopic(id: string) {
|
||||||
|
return await db.topics.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async getTopicMessages(id: string) {
|
||||||
|
const topic = await this.getTopic(id)
|
||||||
|
return topic ? topic.messages : []
|
||||||
|
}
|
||||||
|
|
||||||
|
static async removeTopic(id: string) {
|
||||||
|
const messages = await this.getTopicMessages(id)
|
||||||
|
|
||||||
|
for (const message of messages) {
|
||||||
|
await deleteMessageFiles(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
db.topics.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
static async clearTopicMessages(id: string) {
|
||||||
|
const topic = await this.getTopic(id)
|
||||||
|
|
||||||
|
if (topic) {
|
||||||
|
for (const message of topic?.messages ?? []) {
|
||||||
|
await deleteMessageFiles(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
topic.messages = []
|
||||||
|
|
||||||
|
await db.topics.update(id, topic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,13 +29,17 @@ const resources = {
|
|||||||
select: 'Select',
|
select: 'Select',
|
||||||
search: 'Search',
|
search: 'Search',
|
||||||
default: 'Default',
|
default: 'Default',
|
||||||
warning: 'Warning'
|
warning: 'Warning',
|
||||||
|
back: 'Back',
|
||||||
|
chat: 'Chat'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
add: 'Add',
|
add: 'Add',
|
||||||
added: 'Added',
|
added: 'Added',
|
||||||
manage: 'Manage',
|
manage: 'Manage',
|
||||||
select_model: 'Select Model'
|
select_model: 'Select Model',
|
||||||
|
'show.all': 'Show All',
|
||||||
|
collapse: 'Collapse'
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
copied: 'Copied!',
|
copied: 'Copied!',
|
||||||
@@ -51,9 +55,13 @@ const resources = {
|
|||||||
'chat.completion.paused': 'Chat completion paused',
|
'chat.completion.paused': 'Chat completion paused',
|
||||||
'switch.disabled': 'Switching is disabled while the assistant is generating',
|
'switch.disabled': 'Switching is disabled while the assistant is generating',
|
||||||
'restore.success': 'Restored successfully',
|
'restore.success': 'Restored successfully',
|
||||||
|
'backup.success': 'Backup successful',
|
||||||
'reset.confirm.content': 'Are you sure you want to clear all data?',
|
'reset.confirm.content': 'Are you sure you want to clear all data?',
|
||||||
'reset.double.confirm.title': 'DATA LOST !!!',
|
'reset.double.confirm.title': 'DATA LOST !!!',
|
||||||
'reset.double.confirm.content': 'All data will be lost, do you want to continue?'
|
'reset.double.confirm.content': 'All data will be lost, do you want to continue?',
|
||||||
|
'upgrade.success.title': 'Upgrade successfully',
|
||||||
|
'upgrade.success.content': 'Please restart the application to complete the upgrade',
|
||||||
|
'upgrade.success.button': 'Restart'
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
save: 'Save',
|
save: 'Save',
|
||||||
@@ -62,21 +70,25 @@ const resources = {
|
|||||||
'default.topic.name': 'Default Topic',
|
'default.topic.name': 'Default Topic',
|
||||||
'topics.title': 'Topics',
|
'topics.title': 'Topics',
|
||||||
'topics.auto_rename': 'Auto Rename',
|
'topics.auto_rename': 'Auto Rename',
|
||||||
'topics.edit.title': 'Rename',
|
'topics.edit.title': 'Edit Name',
|
||||||
'topics.edit.placeholder': 'Enter new name',
|
'topics.edit.placeholder': 'Enter new name',
|
||||||
'topics.delete.all.title': 'Delete all topics',
|
'topics.delete.all.title': 'Delete all topics',
|
||||||
'topics.delete.all.content': 'Are you sure you want to delete all topics?',
|
'topics.delete.all.content': 'Are you sure you want to delete all topics?',
|
||||||
|
'topics.move_to': 'Move to',
|
||||||
|
'topics.list': 'Topic List',
|
||||||
'input.new_topic': 'New Topic',
|
'input.new_topic': 'New Topic',
|
||||||
'input.topics': ' Topics ',
|
'input.topics': ' Topics ',
|
||||||
'input.clear': 'Clear',
|
'input.clear': 'Clear',
|
||||||
|
'input.new.context': 'Clear Context',
|
||||||
'input.expand': 'Expand',
|
'input.expand': 'Expand',
|
||||||
'input.collapse': 'Collapse',
|
'input.collapse': 'Collapse',
|
||||||
'input.clear.title': 'Clear all messages?',
|
'input.clear.title': 'Clear all messages?',
|
||||||
'input.clear.content': 'Are you sure to clear all messages?',
|
'input.clear.content': 'Do you want to clear all messages of the current topic?',
|
||||||
'input.placeholder': 'Type your message here...',
|
'input.placeholder': 'Type your message here...',
|
||||||
'input.send': 'Send',
|
'input.send': 'Send',
|
||||||
'input.pause': 'Pause',
|
'input.pause': 'Pause',
|
||||||
'input.settings': 'Settings',
|
'input.settings': 'Settings',
|
||||||
|
'input.upload': 'Upload image or text file',
|
||||||
'input.context_count.tip': 'Context Count',
|
'input.context_count.tip': 'Context Count',
|
||||||
'input.estimated_tokens.tip': 'Estimated tokens',
|
'input.estimated_tokens.tip': 'Estimated tokens',
|
||||||
'settings.temperature': 'Temperature',
|
'settings.temperature': 'Temperature',
|
||||||
@@ -91,20 +103,31 @@ const resources = {
|
|||||||
'settings.set_as_default': 'Apply to default assistant',
|
'settings.set_as_default': 'Apply to default assistant',
|
||||||
'settings.max': 'Max',
|
'settings.max': 'Max',
|
||||||
'suggestions.title': 'Suggested Questions',
|
'suggestions.title': 'Suggested Questions',
|
||||||
'add.assistant.title': 'Add Assistant'
|
'add.assistant.title': 'Add Assistant',
|
||||||
|
'message.new.context': 'New Context',
|
||||||
|
'message.new.branch': 'New Branch',
|
||||||
|
'assistant.search.placeholder': 'Search'
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
title: 'Files',
|
||||||
|
file: 'File',
|
||||||
|
name: 'Name',
|
||||||
|
size: 'Size',
|
||||||
|
count: 'Count',
|
||||||
|
created_at: 'Created At'
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
title: 'Agents',
|
title: 'Assistants',
|
||||||
my_agents: 'My Agents',
|
my_agents: 'My Assistants',
|
||||||
'add.title': 'Add Agent',
|
'add.title': 'Add Assistant',
|
||||||
'edit.title': 'Edit Agent',
|
'edit.title': 'Edit Assistant',
|
||||||
'add.name': 'Name',
|
'add.name': 'Name',
|
||||||
'add.name.placeholder': 'Enter name',
|
'add.name.placeholder': 'Enter name',
|
||||||
'add.prompt': 'Prompt',
|
'add.prompt': 'Prompt',
|
||||||
'add.prompt.placeholder': 'Enter prompt',
|
'add.prompt.placeholder': 'Enter prompt',
|
||||||
'add.button': 'Add',
|
'add.button': 'Add',
|
||||||
'manage.title': 'Manage Agents',
|
'manage.title': 'Manage Assistants',
|
||||||
'delete.popup.content': 'Are you sure you want to delete this agent?',
|
'delete.popup.content': 'Are you sure you want to delete this assistant?',
|
||||||
'tag.default': 'Default',
|
'tag.default': 'Default',
|
||||||
'tag.system': 'System',
|
'tag.system': 'System',
|
||||||
'tag.user': 'Mine'
|
'tag.user': 'Mine'
|
||||||
@@ -127,7 +150,8 @@ const resources = {
|
|||||||
stepfun: 'StepFun',
|
stepfun: 'StepFun',
|
||||||
doubao: 'Doubao',
|
doubao: 'Doubao',
|
||||||
minimax: 'MiniMax',
|
minimax: 'MiniMax',
|
||||||
'graphrag-kylin-mountain': 'GraphRAG'
|
'graphrag-kylin-mountain': 'GraphRAG',
|
||||||
|
github: 'GitHub Models'
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: 'Settings',
|
title: 'Settings',
|
||||||
@@ -143,12 +167,17 @@ const resources = {
|
|||||||
'messages.input.title': 'Input Settings',
|
'messages.input.title': 'Input Settings',
|
||||||
'messages.input.show_estimated_tokens': 'Show estimated input tokens',
|
'messages.input.show_estimated_tokens': 'Show estimated input tokens',
|
||||||
'messages.input.send_shortcuts': 'Send shortcuts',
|
'messages.input.send_shortcuts': 'Send shortcuts',
|
||||||
|
'messages.input.paste_long_text_as_file': 'Paste long text as file',
|
||||||
'general.title': 'General Settings',
|
'general.title': 'General Settings',
|
||||||
'general.user_name': 'User Name',
|
'general.user_name': 'User Name',
|
||||||
'general.user_name.placeholder': 'Enter your name',
|
'general.user_name.placeholder': 'Enter your name',
|
||||||
'general.backup.title': 'Data Backup and Recovery',
|
'general.backup.title': 'Data Backup and Recovery',
|
||||||
|
'general.backup.button': 'Backup',
|
||||||
|
'general.restore.button': 'Restore',
|
||||||
'general.reset.title': 'Data Reset',
|
'general.reset.title': 'Data Reset',
|
||||||
'general.reset.button': 'Reset',
|
'general.reset.button': 'Reset',
|
||||||
|
'advanced.title': 'Advanced Settings',
|
||||||
|
'advanced.click_assistant_switch_to_topics': 'Auto switch to topic',
|
||||||
'provider.api_key': 'API Key',
|
'provider.api_key': 'API Key',
|
||||||
'provider.check': 'Check',
|
'provider.check': 'Check',
|
||||||
'provider.get_api_key': 'Get API Key',
|
'provider.get_api_key': 'Get API Key',
|
||||||
@@ -198,7 +227,13 @@ const resources = {
|
|||||||
'theme.dark': 'Dark',
|
'theme.dark': 'Dark',
|
||||||
'theme.light': 'Light',
|
'theme.light': 'Light',
|
||||||
'theme.auto': 'Auto',
|
'theme.auto': 'Auto',
|
||||||
'font_size.title': 'Message Font Size'
|
'theme.window.style.title': 'Window Style',
|
||||||
|
'theme.window.style.transparent': 'Transparent Window',
|
||||||
|
'theme.window.style.opaque': 'Opaque Window',
|
||||||
|
'font_size.title': 'Message Font Size',
|
||||||
|
'topic.position': 'Topic Position',
|
||||||
|
'topic.position.left': 'Left',
|
||||||
|
'topic.position.right': 'Right'
|
||||||
},
|
},
|
||||||
translate: {
|
translate: {
|
||||||
title: 'Translation',
|
title: 'Translation',
|
||||||
@@ -266,13 +301,17 @@ const resources = {
|
|||||||
select: '选择',
|
select: '选择',
|
||||||
search: '搜索',
|
search: '搜索',
|
||||||
default: '默认',
|
default: '默认',
|
||||||
warning: '警告'
|
warning: '警告',
|
||||||
|
back: '返回',
|
||||||
|
chat: '聊天'
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
add: '添加',
|
add: '添加',
|
||||||
added: '已添加',
|
added: '已添加',
|
||||||
manage: '管理',
|
manage: '管理',
|
||||||
select_model: '选择模型'
|
select_model: '选择模型',
|
||||||
|
'show.all': '显示全部',
|
||||||
|
collapse: '收起'
|
||||||
},
|
},
|
||||||
message: {
|
message: {
|
||||||
copied: '已复制',
|
copied: '已复制',
|
||||||
@@ -288,9 +327,13 @@ const resources = {
|
|||||||
'chat.completion.paused': '会话已停止',
|
'chat.completion.paused': '会话已停止',
|
||||||
'switch.disabled': '模型回复完成后才能切换',
|
'switch.disabled': '模型回复完成后才能切换',
|
||||||
'restore.success': '恢复成功',
|
'restore.success': '恢复成功',
|
||||||
|
'backup.success': '备份成功',
|
||||||
'reset.confirm.content': '确定要重置所有数据吗?',
|
'reset.confirm.content': '确定要重置所有数据吗?',
|
||||||
'reset.double.confirm.title': '数据丢失!!!',
|
'reset.double.confirm.title': '数据丢失!!!',
|
||||||
'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?'
|
'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?',
|
||||||
|
'upgrade.success.title': '升级成功',
|
||||||
|
'upgrade.success.content': '重启应用以完成升级',
|
||||||
|
'upgrade.success.button': '重启'
|
||||||
},
|
},
|
||||||
chat: {
|
chat: {
|
||||||
save: '保存',
|
save: '保存',
|
||||||
@@ -303,17 +346,21 @@ const resources = {
|
|||||||
'topics.edit.placeholder': '输入新名称',
|
'topics.edit.placeholder': '输入新名称',
|
||||||
'topics.delete.all.title': '删除所有话题',
|
'topics.delete.all.title': '删除所有话题',
|
||||||
'topics.delete.all.content': '确定要删除所有话题吗?',
|
'topics.delete.all.content': '确定要删除所有话题吗?',
|
||||||
|
'topics.move_to': '移动到',
|
||||||
|
'topics.list': '话题列表',
|
||||||
'input.new_topic': '新话题',
|
'input.new_topic': '新话题',
|
||||||
'input.topics': ' 话题 ',
|
'input.topics': ' 话题 ',
|
||||||
'input.clear': '清除',
|
'input.clear': '清除会话消息',
|
||||||
|
'input.new.context': '清除上下文',
|
||||||
'input.expand': '展开',
|
'input.expand': '展开',
|
||||||
'input.collapse': '收起',
|
'input.collapse': '收起',
|
||||||
'input.clear.title': '清除所有消息?',
|
'input.clear.title': '清除消息?',
|
||||||
'input.clear.content': '确定要清除所有消息吗?',
|
'input.clear.content': '确定要清除当前会话所有消息吗?',
|
||||||
'input.placeholder': '在这里输入消息...',
|
'input.placeholder': '在这里输入消息...',
|
||||||
'input.send': '发送',
|
'input.send': '发送',
|
||||||
'input.pause': '暂停',
|
'input.pause': '暂停',
|
||||||
'input.settings': '设置',
|
'input.settings': '设置',
|
||||||
|
'input.upload': '上传图片或纯文本文件',
|
||||||
'input.context_count.tip': '上下文数',
|
'input.context_count.tip': '上下文数',
|
||||||
'input.estimated_tokens.tip': '预估 token 数',
|
'input.estimated_tokens.tip': '预估 token 数',
|
||||||
'settings.temperature': '模型温度',
|
'settings.temperature': '模型温度',
|
||||||
@@ -329,7 +376,18 @@ const resources = {
|
|||||||
'settings.set_as_default': '应用到默认助手',
|
'settings.set_as_default': '应用到默认助手',
|
||||||
'settings.max': '不限',
|
'settings.max': '不限',
|
||||||
'suggestions.title': '建议的问题',
|
'suggestions.title': '建议的问题',
|
||||||
'add.assistant.title': '添加智能体'
|
'add.assistant.title': '添加智能体',
|
||||||
|
'message.new.context': '清除上下文',
|
||||||
|
'message.new.branch': '新分支',
|
||||||
|
'assistant.search.placeholder': '搜索'
|
||||||
|
},
|
||||||
|
files: {
|
||||||
|
title: '文件',
|
||||||
|
file: '文件',
|
||||||
|
name: '文件名',
|
||||||
|
size: '大小',
|
||||||
|
count: '文件数',
|
||||||
|
created_at: '创建时间'
|
||||||
},
|
},
|
||||||
agents: {
|
agents: {
|
||||||
title: '智能体',
|
title: '智能体',
|
||||||
@@ -365,7 +423,8 @@ const resources = {
|
|||||||
stepfun: '阶跃星辰',
|
stepfun: '阶跃星辰',
|
||||||
doubao: '豆包',
|
doubao: '豆包',
|
||||||
minimax: 'MiniMax',
|
minimax: 'MiniMax',
|
||||||
'graphrag-kylin-mountain': 'GraphRAG'
|
'graphrag-kylin-mountain': 'GraphRAG',
|
||||||
|
github: 'GitHub Models'
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
title: '设置',
|
title: '设置',
|
||||||
@@ -381,12 +440,17 @@ const resources = {
|
|||||||
'messages.input.title': '输入设置',
|
'messages.input.title': '输入设置',
|
||||||
'messages.input.show_estimated_tokens': '状态显示',
|
'messages.input.show_estimated_tokens': '状态显示',
|
||||||
'messages.input.send_shortcuts': '发送快捷键',
|
'messages.input.send_shortcuts': '发送快捷键',
|
||||||
|
'messages.input.paste_long_text_as_file': '长文本粘贴为文件',
|
||||||
'general.title': '常规设置',
|
'general.title': '常规设置',
|
||||||
'general.user_name': '用户名',
|
'general.user_name': '用户名',
|
||||||
'general.user_name.placeholder': '请输入用户名',
|
'general.user_name.placeholder': '请输入用户名',
|
||||||
'general.backup.title': '数据备份与恢复',
|
'general.backup.title': '数据备份与恢复',
|
||||||
|
'general.backup.button': '备份',
|
||||||
|
'general.restore.button': '恢复',
|
||||||
'general.reset.title': '重置数据',
|
'general.reset.title': '重置数据',
|
||||||
'general.reset.button': '重置',
|
'general.reset.button': '重置',
|
||||||
|
'advanced.title': '高级设置',
|
||||||
|
'advanced.click_assistant_switch_to_topics': '点击助手切换到话题',
|
||||||
'provider.api_key': 'API 密钥',
|
'provider.api_key': 'API 密钥',
|
||||||
'provider.check': '检查',
|
'provider.check': '检查',
|
||||||
'provider.get_api_key': '点击这里获取密钥',
|
'provider.get_api_key': '点击这里获取密钥',
|
||||||
@@ -436,7 +500,13 @@ const resources = {
|
|||||||
'theme.dark': '深色主题',
|
'theme.dark': '深色主题',
|
||||||
'theme.light': '浅色主题',
|
'theme.light': '浅色主题',
|
||||||
'theme.auto': '跟随系统',
|
'theme.auto': '跟随系统',
|
||||||
'font_size.title': '消息字体大小'
|
'theme.window.style.title': '窗口样式',
|
||||||
|
'theme.window.style.transparent': '透明窗口',
|
||||||
|
'theme.window.style.opaque': '不透明窗口',
|
||||||
|
'font_size.title': '消息字体大小',
|
||||||
|
'topic.position': '话题位置',
|
||||||
|
'topic.position.left': '左侧',
|
||||||
|
'topic.position.right': '右侧'
|
||||||
},
|
},
|
||||||
translate: {
|
translate: {
|
||||||
title: '翻译',
|
title: '翻译',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import KeyvStorage from '@kangfenmao/keyv-storage'
|
|||||||
import localforage from 'localforage'
|
import localforage from 'localforage'
|
||||||
|
|
||||||
import { APP_NAME } from './config/env'
|
import { APP_NAME } from './config/env'
|
||||||
import { ThemeMode } from './store/settings'
|
import { ThemeMode } from './types'
|
||||||
import { loadScript } from './utils'
|
import { loadScript } from './utils'
|
||||||
|
|
||||||
export async function initMermaid(theme: ThemeMode) {
|
export async function initMermaid(theme: ThemeMode) {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ const AppsPage: FC = () => {
|
|||||||
icon: null,
|
icon: null,
|
||||||
closable: true,
|
closable: true,
|
||||||
maskClosable: true,
|
maskClosable: true,
|
||||||
|
centered: true,
|
||||||
okButtonProps: { type: 'primary', disabled: Boolean(added) },
|
okButtonProps: { type: 'primary', disabled: Boolean(added) },
|
||||||
okText: added ? t('button.added') : t('button.add'),
|
okText: added ? t('button.added') : t('button.add'),
|
||||||
onOk: () => onAddAgent(agent)
|
onOk: () => onAddAgent(agent)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'emoji-picker-element'
|
import 'emoji-picker-element'
|
||||||
|
|
||||||
|
import { LoadingOutlined, ThunderboltOutlined } from '@ant-design/icons'
|
||||||
import EmojiPicker from '@renderer/components/EmojiPicker'
|
import EmojiPicker from '@renderer/components/EmojiPicker'
|
||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { useAgents } from '@renderer/hooks/useAgents'
|
import { useAgents } from '@renderer/hooks/useAgents'
|
||||||
|
import { fetchGenerate } from '@renderer/services/api'
|
||||||
import { syncAgentToAssistant } from '@renderer/services/assistant'
|
import { syncAgentToAssistant } from '@renderer/services/assistant'
|
||||||
import { Agent } from '@renderer/types'
|
import { Agent } from '@renderer/types'
|
||||||
import { getLeadingEmoji, uuid } from '@renderer/utils'
|
import { getLeadingEmoji, uuid } from '@renderer/utils'
|
||||||
@@ -29,6 +31,7 @@ const PopupContainer: React.FC<Props> = ({ agent, resolve }) => {
|
|||||||
const { addAgent, updateAgent } = useAgents()
|
const { addAgent, updateAgent } = useAgents()
|
||||||
const formRef = useRef<FormInstance>(null)
|
const formRef = useRef<FormInstance>(null)
|
||||||
const [emoji, setEmoji] = useState(agent?.emoji)
|
const [emoji, setEmoji] = useState(agent?.emoji)
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
const onFinish = (values: FieldType) => {
|
const onFinish = (values: FieldType) => {
|
||||||
const _emoji = emoji || getLeadingEmoji(values.name)
|
const _emoji = emoji || getLeadingEmoji(values.name)
|
||||||
@@ -81,16 +84,44 @@ const PopupContainer: React.FC<Props> = ({ agent, resolve }) => {
|
|||||||
}
|
}
|
||||||
}, [agent, form])
|
}, [agent, form])
|
||||||
|
|
||||||
|
const handleButtonClick = async () => {
|
||||||
|
const prompt = `你是一个专业的 prompt 优化助手,我会给你一段prompt,你需要帮我优化它,仅回复优化后的 prompt 不要添加任何解释,使用 [CRISPE提示框架] 回复。`
|
||||||
|
|
||||||
|
const name = formRef.current?.getFieldValue('name')
|
||||||
|
const content = formRef.current?.getFieldValue('prompt')
|
||||||
|
const promptText = content || name
|
||||||
|
|
||||||
|
if (!promptText) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
navigator.clipboard.writeText(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const prefixedContent = `请帮我优化下面这段 prompt,使用 CRISPE 提示框架,请使用 Markdown 格式回复,不要使用 codeblock: ${promptText}`
|
||||||
|
const generatedText = await fetchGenerate({ prompt, content: prefixedContent })
|
||||||
|
formRef.current?.setFieldValue('prompt', generatedText)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error)
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
style={{ marginTop: '10vh' }}
|
|
||||||
title={agent ? t('agents.edit.title') : t('agents.add.title')}
|
title={agent ? t('agents.edit.title') : t('agents.add.title')}
|
||||||
open={open}
|
open={open}
|
||||||
onOk={() => formRef.current?.submit()}
|
onOk={() => formRef.current?.submit()}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
afterClose={onClose}
|
afterClose={onClose}
|
||||||
okText={agent ? t('common.save') : t('agents.add.button')}>
|
okText={agent ? t('common.save') : t('agents.add.button')}
|
||||||
|
centered>
|
||||||
<Form
|
<Form
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
form={form}
|
form={form}
|
||||||
@@ -100,16 +131,28 @@ const PopupContainer: React.FC<Props> = ({ agent, resolve }) => {
|
|||||||
style={{ marginTop: 25 }}
|
style={{ marginTop: 25 }}
|
||||||
onFinish={onFinish}>
|
onFinish={onFinish}>
|
||||||
<Form.Item name="name" label="Emoji">
|
<Form.Item name="name" label="Emoji">
|
||||||
<Popover content={<EmojiPicker onEmojiClick={setEmoji} />} trigger="click" arrow>
|
<Popover content={<EmojiPicker onEmojiClick={setEmoji} />} arrow>
|
||||||
<Button icon={emoji && <span style={{ fontSize: 20 }}>{emoji}</span>}>{t('common.select')}</Button>
|
<Button icon={emoji && <span style={{ fontSize: 20 }}>{emoji}</span>}>{t('common.select')}</Button>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="name" label={t('agents.add.name')} rules={[{ required: true }]}>
|
<Form.Item name="name" label={t('agents.add.name')} rules={[{ required: true }]}>
|
||||||
<Input placeholder={t('agents.add.name.placeholder')} spellCheck={false} allowClear />
|
<Input placeholder={t('agents.add.name.placeholder')} spellCheck={false} allowClear />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="prompt" label={t('agents.add.prompt')} rules={[{ required: true }]}>
|
<div style={{ position: 'relative' }}>
|
||||||
<TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={4} />
|
<Form.Item
|
||||||
</Form.Item>
|
name="prompt"
|
||||||
|
label={t('agents.add.prompt')}
|
||||||
|
rules={[{ required: true }]}
|
||||||
|
style={{ position: 'relative' }}>
|
||||||
|
<TextArea placeholder={t('agents.add.prompt.placeholder')} spellCheck={false} rows={10} />
|
||||||
|
</Form.Item>
|
||||||
|
<Button
|
||||||
|
icon={loading ? <LoadingOutlined /> : <ThunderboltOutlined />}
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
style={{ position: 'absolute', top: 8, right: 8 }}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ const PopupContainer: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
style={{ marginTop: '10vh' }}
|
|
||||||
title={t('agents.manage.title')}
|
title={t('agents.manage.title')}
|
||||||
open={open}
|
open={open}
|
||||||
onOk={onOk}
|
onOk={onOk}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
afterClose={onClose}
|
afterClose={onClose}
|
||||||
footer={null}>
|
footer={null}
|
||||||
|
centered>
|
||||||
<Container>
|
<Container>
|
||||||
{agents.length > 0 && (
|
{agents.length > 0 && (
|
||||||
<DragableList list={agents} onUpdate={updateAgents}>
|
<DragableList list={agents} onUpdate={updateAgents}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import MinApp from '@renderer/components/MinApp'
|
import MinApp from '@renderer/components/MinApp'
|
||||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { MinAppType } from '@renderer/types'
|
import { MinAppType } from '@renderer/types'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@@ -12,13 +12,6 @@ const App: FC<Props> = ({ app }) => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
const websiteReg = /claude|chatgpt|groq/i
|
|
||||||
|
|
||||||
if (websiteReg.test(app.url)) {
|
|
||||||
window.api.minApp({ url: app.url, windowOptions: { title: app.name } })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
MinApp.start(app)
|
MinApp.start(app)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,87 +1,25 @@
|
|||||||
import { SearchOutlined } from '@ant-design/icons'
|
import { SearchOutlined } from '@ant-design/icons'
|
||||||
import AiAssistantAppLogo from '@renderer/assets/images/apps/360-ai.png'
|
|
||||||
import AiSearchAppLogo from '@renderer/assets/images/apps/ai-search.png'
|
|
||||||
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png'
|
|
||||||
import DevvAppLogo from '@renderer/assets/images/apps/devv.png'
|
|
||||||
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
|
|
||||||
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
|
|
||||||
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png'
|
|
||||||
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png'
|
|
||||||
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png'
|
|
||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
import { Center } from '@renderer/components/Layout'
|
import { Center } from '@renderer/components/Layout'
|
||||||
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
import { getAllMinApps } from '@renderer/config/minapp'
|
||||||
import { MinAppType } from '@renderer/types'
|
|
||||||
import { Empty, Input } from 'antd'
|
import { Empty, Input } from 'antd'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useMemo, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
|
||||||
const _apps: MinAppType[] = [
|
|
||||||
{
|
|
||||||
name: 'AI 助手',
|
|
||||||
logo: AiAssistantAppLogo,
|
|
||||||
url: 'https://bot.360.com/'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '文心一言',
|
|
||||||
logo: BaiduAiAppLogo,
|
|
||||||
url: 'https://yiyan.baidu.com/'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'SparkDesk',
|
|
||||||
logo: SparkDeskAppLogo,
|
|
||||||
url: 'https://xinghuo.xfyun.cn/desk'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '腾讯元宝',
|
|
||||||
logo: TencentYuanbaoAppLogo,
|
|
||||||
url: 'https://yuanbao.tencent.com/chat'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '商量',
|
|
||||||
logo: SensetimeAppLogo,
|
|
||||||
url: 'https://chat.sensetime.com/wb/chat'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '360AI搜索',
|
|
||||||
logo: AiSearchAppLogo,
|
|
||||||
url: 'https://so.360.com/'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '秘塔AI搜索',
|
|
||||||
logo: MetasoAppLogo,
|
|
||||||
url: 'https://metaso.cn/'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '天工AI',
|
|
||||||
logo: TiangongAiLogo,
|
|
||||||
url: 'https://www.tiangong.cn/'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'DEVV_',
|
|
||||||
logo: DevvAppLogo,
|
|
||||||
url: 'https://devv.ai/referral?code=dvl5am34asqo'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const AppsPage: FC = () => {
|
const AppsPage: FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const apps = useMemo(() => getAllMinApps(), [])
|
||||||
|
|
||||||
const list: MinAppType[] = (Object.entries(PROVIDER_CONFIG) as any[])
|
const filteredApps = search
|
||||||
.filter(([, config]) => config.app)
|
? apps.filter(
|
||||||
.map(([key, config]) => ({ id: key, ...config.app }))
|
|
||||||
.concat(_apps)
|
|
||||||
|
|
||||||
const apps = search
|
|
||||||
? list.filter(
|
|
||||||
(app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase())
|
(app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase())
|
||||||
)
|
)
|
||||||
: list
|
: apps
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@@ -103,10 +41,10 @@ const AppsPage: FC = () => {
|
|||||||
</Navbar>
|
</Navbar>
|
||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
<AppsContainer>
|
<AppsContainer>
|
||||||
{apps.map((app) => (
|
{filteredApps.map((app) => (
|
||||||
<App key={app.name} app={app} />
|
<App key={app.id} app={app} />
|
||||||
))}
|
))}
|
||||||
{isEmpty(apps) && (
|
{isEmpty(filteredApps) && (
|
||||||
<Center style={{ flex: 1 }}>
|
<Center style={{ flex: 1 }}>
|
||||||
<Empty />
|
<Empty />
|
||||||
</Center>
|
</Center>
|
||||||
|
|||||||
100
src/renderer/src/pages/files/FilesPage.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
|
import { VStack } from '@renderer/components/Layout'
|
||||||
|
import db from '@renderer/databases'
|
||||||
|
import FileManager from '@renderer/services/file'
|
||||||
|
import { FileType, FileTypes } from '@renderer/types'
|
||||||
|
import { Image, Table } from 'antd'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const FilesPage: FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const files = useLiveQuery<FileType[]>(() => db.files.orderBy('created_at').reverse().toArray())
|
||||||
|
|
||||||
|
const dataSource = files?.map((file) => {
|
||||||
|
const isImage = file.type === FileTypes.IMAGE
|
||||||
|
const ImageView = <Image src={'file://' + file.path} preview={false} style={{ maxHeight: '40px' }} />
|
||||||
|
return {
|
||||||
|
key: file.id,
|
||||||
|
file: isImage ? ImageView : <FileNameText className="text-nowrap">{file.origin_name}</FileNameText>,
|
||||||
|
name: <a href={'file://' + FileManager.getSafePath(file)}>{file.origin_name}</a>,
|
||||||
|
size: `${(file.size / 1024 / 1024).toFixed(2)} MB`,
|
||||||
|
count: file.count,
|
||||||
|
created_at: dayjs(file.created_at).format('MM-DD HH:mm')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: t('files.file'),
|
||||||
|
dataIndex: 'file',
|
||||||
|
key: 'file',
|
||||||
|
width: '300px'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('files.name'),
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('files.size'),
|
||||||
|
dataIndex: 'size',
|
||||||
|
key: 'size',
|
||||||
|
width: '100px'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('files.count'),
|
||||||
|
dataIndex: 'count',
|
||||||
|
key: 'count',
|
||||||
|
width: '100px'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t('files.created_at'),
|
||||||
|
dataIndex: 'created_at',
|
||||||
|
key: 'created_at',
|
||||||
|
width: '120px'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Navbar>
|
||||||
|
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
|
||||||
|
</Navbar>
|
||||||
|
<ContentContainer>
|
||||||
|
<VStack style={{ flex: 1 }}>
|
||||||
|
<Table dataSource={dataSource} columns={columns} style={{ width: '100%', height: '100%' }} size="small" />
|
||||||
|
</VStack>
|
||||||
|
</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);
|
||||||
|
padding: 20px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const FileNameText = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-text);
|
||||||
|
max-width: 300px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default FilesPage
|
||||||
@@ -1,16 +1,19 @@
|
|||||||
import { CopyOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
import { DeleteOutlined, EditOutlined, MinusCircleOutlined } from '@ant-design/icons'
|
||||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
import DragableList from '@renderer/components/DragableList'
|
||||||
|
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||||
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
|
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
|
||||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
|
import { getDefaultTopic, syncAsistantToAgent } from '@renderer/services/assistant'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
import { useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import { setSearching } from '@renderer/store/runtime'
|
||||||
import { Assistant } from '@renderer/types'
|
import { Assistant } from '@renderer/types'
|
||||||
import { droppableReorder, uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
import { Dropdown } from 'antd'
|
import { Dropdown, Input, InputRef } from 'antd'
|
||||||
import { ItemType } from 'antd/es/menu/interface'
|
import { ItemType } from 'antd/es/menu/interface'
|
||||||
import { last } from 'lodash'
|
import { isEmpty, last } from 'lodash'
|
||||||
import { FC, useCallback } from 'react'
|
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@@ -23,8 +26,12 @@ interface Props {
|
|||||||
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
|
const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAssistant }) => {
|
||||||
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
|
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
|
||||||
const generating = useAppSelector((state) => state.runtime.generating)
|
const generating = useAppSelector((state) => state.runtime.generating)
|
||||||
const { updateAssistant } = useAssistant(activeAssistant.id)
|
const [search, setSearch] = useState('')
|
||||||
|
const { updateAssistant, removeAllTopics } = useAssistant(activeAssistant.id)
|
||||||
|
const { clickAssistantToShowTopic, topicPosition } = useSettings()
|
||||||
|
const searchRef = useRef<InputRef>(null)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const onDelete = useCallback(
|
const onDelete = useCallback(
|
||||||
(assistant: Assistant) => {
|
(assistant: Assistant) => {
|
||||||
@@ -35,6 +42,15 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
|||||||
[assistants, onCreateAssistant, removeAssistant, setActiveAssistant]
|
[assistants, onCreateAssistant, removeAssistant, setActiveAssistant]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onEditAssistant = useCallback(
|
||||||
|
async (assistant: Assistant) => {
|
||||||
|
const _assistant = await AssistantSettingPopup.show({ assistant })
|
||||||
|
updateAssistant(_assistant)
|
||||||
|
syncAsistantToAgent(_assistant)
|
||||||
|
},
|
||||||
|
[updateAssistant]
|
||||||
|
)
|
||||||
|
|
||||||
const getMenuItems = useCallback(
|
const getMenuItems = useCallback(
|
||||||
(assistant: Assistant) =>
|
(assistant: Assistant) =>
|
||||||
[
|
[
|
||||||
@@ -42,22 +58,32 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
|||||||
label: t('common.edit'),
|
label: t('common.edit'),
|
||||||
key: 'edit',
|
key: 'edit',
|
||||||
icon: <EditOutlined />,
|
icon: <EditOutlined />,
|
||||||
async onClick() {
|
onClick: () => onEditAssistant(assistant)
|
||||||
const _assistant = await AssistantSettingPopup.show({ assistant })
|
|
||||||
updateAssistant(_assistant)
|
|
||||||
syncAsistantToAgent(_assistant)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('common.duplicate'),
|
label: t('common.duplicate'),
|
||||||
key: 'duplicate',
|
key: 'duplicate',
|
||||||
icon: <CopyOutlined />,
|
icon: <CopyIcon />,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
|
const _assistant: Assistant = { ...assistant, id: uuid(), topics: [getDefaultTopic()] }
|
||||||
addAssistant(_assistant)
|
addAssistant(_assistant)
|
||||||
setActiveAssistant(_assistant)
|
setActiveAssistant(_assistant)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t('chat.topics.delete.all.title'),
|
||||||
|
key: 'delete-all',
|
||||||
|
icon: <MinusCircleOutlined />,
|
||||||
|
onClick: () => {
|
||||||
|
window.modal.confirm({
|
||||||
|
title: t('chat.topics.delete.all.title'),
|
||||||
|
content: t('chat.topics.delete.all.content'),
|
||||||
|
centered: true,
|
||||||
|
okButtonProps: { danger: true },
|
||||||
|
onOk: removeAllTopics
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
{ type: 'divider' },
|
{ type: 'divider' },
|
||||||
{
|
{
|
||||||
label: t('common.delete'),
|
label: t('common.delete'),
|
||||||
@@ -67,19 +93,7 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
|||||||
onClick: () => onDelete(assistant)
|
onClick: () => onDelete(assistant)
|
||||||
}
|
}
|
||||||
] as ItemType[],
|
] as ItemType[],
|
||||||
[addAssistant, onDelete, setActiveAssistant, t, updateAssistant]
|
[addAssistant, onDelete, onEditAssistant, removeAllTopics, setActiveAssistant, t]
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[assistants, updateAssistants]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSwitchAssistant = useCallback(
|
const onSwitchAssistant = useCallback(
|
||||||
@@ -90,41 +104,95 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
|||||||
key: 'switch-assistant'
|
key: 'switch-assistant'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
|
|
||||||
|
if (topicPosition === 'left' && clickAssistantToShowTopic) {
|
||||||
|
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
|
||||||
|
}
|
||||||
|
|
||||||
setActiveAssistant(assistant)
|
setActiveAssistant(assistant)
|
||||||
},
|
},
|
||||||
[generating, setActiveAssistant, t]
|
[clickAssistantToShowTopic, generating, setActiveAssistant, t, topicPosition]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const list = assistants.filter((assistant) => assistant.name?.toLowerCase().includes(search.toLowerCase().trim()))
|
||||||
|
|
||||||
|
const onSearch = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
const isEnterPressed = e.keyCode == 13
|
||||||
|
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
return searchRef.current?.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEnterPressed) {
|
||||||
|
if (list.length > 0) {
|
||||||
|
if (list.length === 1) {
|
||||||
|
onSwitchAssistant(list[0])
|
||||||
|
setSearch('')
|
||||||
|
setTimeout(() => searchRef.current?.blur(), 0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const index = list.findIndex((a) => a.id === activeAssistant?.id)
|
||||||
|
onSwitchAssistant(index === list.length - 1 ? list[0] : list[index + 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||||
|
searchRef.current?.focus()
|
||||||
|
searchRef.current?.select()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Command or Ctrl + K create new topic
|
||||||
|
useEffect(() => {
|
||||||
|
const onKeydown = (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
||||||
|
searchRef.current?.focus()
|
||||||
|
searchRef.current?.select()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('keydown', onKeydown)
|
||||||
|
return () => document.removeEventListener('keydown', onKeydown)
|
||||||
|
}, [activeAssistant?.id, list, onSwitchAssistant])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<DragDropContext onDragEnd={onDragEnd}>
|
{assistants.length >= 10 && (
|
||||||
<Droppable droppableId="droppable">
|
<SearchContainer>
|
||||||
{(provided) => (
|
<Input
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
placeholder={t('chat.assistant.search.placeholder')}
|
||||||
{assistants.map((assistant, index) => (
|
suffix={<CommandKey>⌘+K</CommandKey>}
|
||||||
<Draggable key={`draggable_${assistant.id}_${index}`} draggableId={assistant.id} index={index}>
|
value={search}
|
||||||
{(provided) => (
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
<div
|
style={{ borderRadius: 4, borderWidth: 0.5 }}
|
||||||
ref={provided.innerRef}
|
onKeyDown={onSearch}
|
||||||
{...provided.draggableProps}
|
ref={searchRef}
|
||||||
{...provided.dragHandleProps}
|
onFocus={() => dispatch(setSearching(true))}
|
||||||
style={{ ...provided.draggableProps.style, marginBottom: 5 }}>
|
onBlur={() => {
|
||||||
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
|
dispatch(setSearching(false))
|
||||||
<AssistantItem
|
setSearch('')
|
||||||
onClick={() => onSwitchAssistant(assistant)}
|
}}
|
||||||
className={assistant.id === activeAssistant?.id ? 'active' : ''}>
|
allowClear
|
||||||
<AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName>
|
/>
|
||||||
</AssistantItem>
|
</SearchContainer>
|
||||||
</Dropdown>
|
)}
|
||||||
</div>
|
<DragableList list={list} onUpdate={updateAssistants} droppableProps={{ isDropDisabled: !isEmpty(search) }}>
|
||||||
)}
|
{(assistant) => {
|
||||||
</Draggable>
|
const isCurrent = assistant.id === activeAssistant?.id
|
||||||
))}
|
return (
|
||||||
</div>
|
<Dropdown key={assistant.id} menu={{ items: getMenuItems(assistant) }} trigger={['contextMenu']}>
|
||||||
)}
|
<AssistantItem onClick={() => onSwitchAssistant(assistant)} className={isCurrent ? 'active' : ''}>
|
||||||
</Droppable>
|
<AssistantName className="name">{assistant.name || t('chat.default.name')}</AssistantName>
|
||||||
</DragDropContext>
|
<ArrowRightButton
|
||||||
|
className={`arrow-button ${isCurrent ? 'active' : ''}`}
|
||||||
|
onClick={() => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)}>
|
||||||
|
<i className="iconfont icon-gridlines" />
|
||||||
|
</ArrowRightButton>
|
||||||
|
{false && <TopicCount className="topics-count">{assistant.topics.length}</TopicCount>}
|
||||||
|
</AssistantItem>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</DragableList>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -132,38 +200,39 @@ const Assistants: FC<Props> = ({ activeAssistant, setActiveAssistant, onCreateAs
|
|||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: var(--assistants-width);
|
|
||||||
max-width: var(--assistants-width);
|
|
||||||
border-right: 0.5px solid var(--color-border);
|
|
||||||
height: calc(100vh - var(--navbar-height));
|
height: calc(100vh - var(--navbar-height));
|
||||||
padding: 10px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
padding-top: 10px;
|
||||||
|
padding-bottom: 10px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const AssistantItem = styled.div`
|
const AssistantItem = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
margin: 0 10px;
|
||||||
|
padding-right: 35px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: Ubuntu;
|
font-family: Ubuntu;
|
||||||
.anticon {
|
.iconfont {
|
||||||
display: none;
|
opacity: 0;
|
||||||
}
|
color: var(--color-text-3);
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
.anticon {
|
|
||||||
display: block;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.active {
|
&.active {
|
||||||
background-color: var(--color-background-mute);
|
background-color: var(--color-background-mute);
|
||||||
cursor: pointer;
|
|
||||||
.name {
|
.name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
.topics-count {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.iconfont {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -173,6 +242,55 @@ const AssistantName = styled.div`
|
|||||||
-webkit-line-clamp: 1;
|
-webkit-line-clamp: 1;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
font-size: 13px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ArrowRightButton = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
min-width: 22px;
|
||||||
|
min-height: 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
right: 9px;
|
||||||
|
top: 6px;
|
||||||
|
.iconfont {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TopicCount = styled.div`
|
||||||
|
color: var(--color-text-2);
|
||||||
|
font-size: 10px;
|
||||||
|
margin-right: 3px;
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
opacity: 0.8;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SearchContainer = styled.div`
|
||||||
|
margin: 0 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const CommandKey = styled.div`
|
||||||
|
color: var(--color-text-2);
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
margin-right: -4px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default Assistants
|
export default Assistants
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { Assistant } from '@renderer/types'
|
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||||
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
import { Flex } from 'antd'
|
import { Flex } from 'antd'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@@ -11,19 +12,31 @@ import RightSidebar from './RightSidebar'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
|
activeTopic: Topic
|
||||||
|
setActiveTopic: (topic: Topic) => void
|
||||||
|
setActiveAssistant: (assistant: Assistant) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Chat: FC<Props> = (props) => {
|
const Chat: FC<Props> = (props) => {
|
||||||
const { assistant } = useAssistant(props.assistant.id)
|
const { assistant } = useAssistant(props.assistant.id)
|
||||||
const { activeTopic, setActiveTopic } = useActiveTopic(assistant)
|
const { topicPosition } = useSettings()
|
||||||
|
const { showTopics } = useShowTopics()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container id="chat">
|
<Container id="chat">
|
||||||
<Main vertical flex={1} justify="space-between">
|
<Main vertical flex={1} justify="space-between">
|
||||||
<Messages assistant={assistant} topic={activeTopic} />
|
<Messages assistant={assistant} topic={props.activeTopic} setActiveTopic={props.setActiveTopic} />
|
||||||
<Inputbar assistant={assistant} setActiveTopic={setActiveTopic} />
|
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} />
|
||||||
</Main>
|
</Main>
|
||||||
<RightSidebar assistant={assistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
|
{topicPosition === 'right' && showTopics && (
|
||||||
|
<RightSidebar
|
||||||
|
activeAssistant={assistant}
|
||||||
|
activeTopic={props.activeTopic}
|
||||||
|
setActiveAssistant={props.setActiveAssistant}
|
||||||
|
setActiveTopic={props.setActiveTopic}
|
||||||
|
position="right"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
import { NavbarCenter } from '@renderer/components/app/Navbar'
|
|
||||||
import { isMac } from '@renderer/config/constant'
|
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
|
||||||
import { useShowAssistants } from '@renderer/hooks/useStore'
|
|
||||||
import { Assistant } from '@renderer/types'
|
|
||||||
import { removeLeadingEmoji } from '@renderer/utils'
|
|
||||||
import { FC } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
import SelectModelButton from './components/SelectModelButton'
|
|
||||||
import { NewButton } from './HomePage'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
activeAssistant: Assistant
|
|
||||||
}
|
|
||||||
|
|
||||||
const HomeHeader: FC<Props> = ({ activeAssistant }) => {
|
|
||||||
const { assistant } = useAssistant(activeAssistant.id)
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NavbarCenter style={{ paddingLeft: isMac ? 16 : 8 }}>
|
|
||||||
{!showAssistants && (
|
|
||||||
<NewButton onClick={toggleShowAssistants} style={{ marginRight: isMac ? 8 : 25 }}>
|
|
||||||
<i className="iconfont icon-showsidebarhoriz" />
|
|
||||||
</NewButton>
|
|
||||||
)}
|
|
||||||
<AssistantName>{removeLeadingEmoji(assistant?.name) || t('chat.default.name')}</AssistantName>
|
|
||||||
<SelectModelButton assistant={assistant} />
|
|
||||||
</NavbarCenter>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const AssistantName = styled.span`
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: 10px;
|
|
||||||
font-family: Ubuntu;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default HomeHeader
|
|
||||||
@@ -1,77 +1,43 @@
|
|||||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import { isMac, isWindows } from '@renderer/config/constant'
|
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||||
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||||
import { useShowAssistants, useShowRightSidebar } from '@renderer/hooks/useStore'
|
|
||||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
|
||||||
import { Assistant } from '@renderer/types'
|
import { Assistant } from '@renderer/types'
|
||||||
import { uuid } from '@renderer/utils'
|
|
||||||
import { Switch } from 'antd'
|
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import AddAssistantPopup from '../../components/Popups/AddAssistantPopup'
|
|
||||||
import Assistants from './Assistants'
|
|
||||||
import Chat from './Chat'
|
import Chat from './Chat'
|
||||||
import Navigation from './Header'
|
import Navbar from './Navbar'
|
||||||
|
import RightSidebar from './RightSidebar'
|
||||||
|
|
||||||
let _activeAssistant: Assistant
|
let _activeAssistant: Assistant
|
||||||
|
|
||||||
const HomePage: FC = () => {
|
const HomePage: FC = () => {
|
||||||
const { assistants, addAssistant } = useAssistants()
|
const { assistants } = useAssistants()
|
||||||
const [activeAssistant, setActiveAssistant] = useState(_activeAssistant || assistants[0])
|
const [activeAssistant, setActiveAssistant] = useState(_activeAssistant || assistants[0])
|
||||||
const { rightSidebarShown, toggleRightSidebar } = useShowRightSidebar()
|
const { showAssistants } = useShowAssistants()
|
||||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant)
|
||||||
const { defaultAssistant } = useDefaultAssistant()
|
|
||||||
const { theme, toggleTheme } = useTheme()
|
|
||||||
|
|
||||||
_activeAssistant = activeAssistant
|
_activeAssistant = activeAssistant
|
||||||
|
|
||||||
const onCreateDefaultAssistant = () => {
|
|
||||||
const assistant = { ...defaultAssistant, id: uuid() }
|
|
||||||
addAssistant(assistant)
|
|
||||||
setActiveAssistant(assistant)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCreateAssistant = async () => {
|
|
||||||
const assistant = await AddAssistantPopup.show()
|
|
||||||
assistant && setActiveAssistant(assistant)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Navbar>
|
<Navbar activeAssistant={activeAssistant} setActiveAssistant={setActiveAssistant} activeTopic={activeTopic} />
|
||||||
{showAssistants && (
|
|
||||||
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
|
|
||||||
<NewButton onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
|
|
||||||
<i className="iconfont icon-hidesidebarhoriz" />
|
|
||||||
</NewButton>
|
|
||||||
<NewButton onClick={onCreateAssistant}>
|
|
||||||
<i className="iconfont icon-a-addchat"></i>
|
|
||||||
</NewButton>
|
|
||||||
</NavbarLeft>
|
|
||||||
)}
|
|
||||||
<Navigation activeAssistant={activeAssistant} />
|
|
||||||
<NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 12 }}>
|
|
||||||
<ThemeSwitch
|
|
||||||
checkedChildren={<i className="iconfont icon-theme icon-dark1" />}
|
|
||||||
unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />}
|
|
||||||
checked={theme === 'dark'}
|
|
||||||
onChange={toggleTheme}
|
|
||||||
/>
|
|
||||||
<NewButton onClick={toggleRightSidebar}>
|
|
||||||
<i className={`iconfont ${rightSidebarShown ? 'icon-showsidebarhoriz' : 'icon-hidesidebarhoriz'}`} />
|
|
||||||
</NewButton>
|
|
||||||
</NavbarRight>
|
|
||||||
</Navbar>
|
|
||||||
<ContentContainer>
|
<ContentContainer>
|
||||||
{showAssistants && (
|
{showAssistants && (
|
||||||
<Assistants
|
<RightSidebar
|
||||||
activeAssistant={activeAssistant}
|
activeAssistant={activeAssistant}
|
||||||
|
activeTopic={activeTopic}
|
||||||
setActiveAssistant={setActiveAssistant}
|
setActiveAssistant={setActiveAssistant}
|
||||||
onCreateAssistant={onCreateDefaultAssistant}
|
setActiveTopic={setActiveTopic}
|
||||||
|
position="left"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Chat assistant={activeAssistant} />
|
<Chat
|
||||||
|
assistant={activeAssistant}
|
||||||
|
activeTopic={activeTopic}
|
||||||
|
setActiveTopic={setActiveTopic}
|
||||||
|
setActiveAssistant={setActiveAssistant}
|
||||||
|
/>
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
@@ -90,40 +56,4 @@ const ContentContainer = styled.div`
|
|||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
`
|
`
|
||||||
|
|
||||||
export const NewButton = styled.div`
|
|
||||||
-webkit-app-region: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
transition: all 0.2s ease-in-out;
|
|
||||||
color: var(--color-icon);
|
|
||||||
.icon-a-addchat {
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
.anticon {
|
|
||||||
font-size: 19px;
|
|
||||||
}
|
|
||||||
.icon-showsidebarhoriz,
|
|
||||||
.icon-hidesidebarhoriz {
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
cursor: pointer;
|
|
||||||
color: var(--color-icon-white);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const ThemeSwitch = styled(Switch)`
|
|
||||||
-webkit-app-region: none;
|
|
||||||
margin-right: 10px;
|
|
||||||
.icon-theme {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default HomePage
|
export default HomePage
|
||||||
|
|||||||
46
src/renderer/src/pages/home/Inputbar/AttachmentButton.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { PaperClipOutlined } from '@ant-design/icons'
|
||||||
|
import { imageExts, textExts } from '@renderer/config/constant'
|
||||||
|
import { isVisionModel } from '@renderer/config/models'
|
||||||
|
import { FileType, Model } from '@renderer/types'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
model: Model
|
||||||
|
files: FileType[]
|
||||||
|
setFiles: (files: FileType[]) => void
|
||||||
|
ToolbarButton: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const AttachmentButton: FC<Props> = ({ model, files, setFiles, ToolbarButton }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const extensions = isVisionModel(model) ? [...imageExts, ...textExts] : [...textExts]
|
||||||
|
|
||||||
|
const onSelectFile = async () => {
|
||||||
|
if (files.length > 0) {
|
||||||
|
return setFiles([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const _files = await window.api.file.select({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: 'Files',
|
||||||
|
extensions: extensions.map((i) => i.replace('.', ''))
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
_files && setFiles(_files)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip placement="top" title={t('chat.input.upload')} arrow>
|
||||||
|
<ToolbarButton type="text" className={files.length ? 'active' : ''} onClick={onSelectFile}>
|
||||||
|
<PaperClipOutlined style={{ rotate: '135deg' }} />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AttachmentButton
|
||||||
42
src/renderer/src/pages/home/Inputbar/AttachmentPreview.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import FileManager from '@renderer/services/file'
|
||||||
|
import { FileType } from '@renderer/types'
|
||||||
|
import { Upload } from 'antd'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
files: FileType[]
|
||||||
|
setFiles: (files: FileType[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
|
||||||
|
if (isEmpty(files)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Upload
|
||||||
|
listType="picture-card"
|
||||||
|
fileList={files.map((file) => ({
|
||||||
|
uid: file.id,
|
||||||
|
url: 'file://' + FileManager.getSafePath(file),
|
||||||
|
status: 'done',
|
||||||
|
name: file.name
|
||||||
|
}))}
|
||||||
|
onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 10px 20px;
|
||||||
|
margin-right: 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default AttachmentPreview
|
||||||
@@ -1,32 +1,39 @@
|
|||||||
import {
|
import {
|
||||||
ClearOutlined,
|
ClearOutlined,
|
||||||
ControlOutlined,
|
ControlOutlined,
|
||||||
|
FormOutlined,
|
||||||
FullscreenExitOutlined,
|
FullscreenExitOutlined,
|
||||||
FullscreenOutlined,
|
FullscreenOutlined,
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
PauseCircleOutlined,
|
PauseCircleOutlined,
|
||||||
PlusCircleOutlined,
|
|
||||||
QuestionCircleOutlined
|
QuestionCircleOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { DEFAULT_CONEXTCOUNT } from '@renderer/config/constant'
|
import { imageExts, textExts } from '@renderer/config/constant'
|
||||||
|
import { isVisionModel } from '@renderer/config/models'
|
||||||
|
import db from '@renderer/databases'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useRuntime, useShowTopics } from '@renderer/hooks/useStore'
|
||||||
import { getDefaultTopic } from '@renderer/services/assistant'
|
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
import { estimateInputTokenCount } from '@renderer/services/messages'
|
import FileManager from '@renderer/services/file'
|
||||||
import store, { useAppSelector } from '@renderer/store'
|
import { estimateTextTokens } from '@renderer/services/tokens'
|
||||||
import { setGenerating } from '@renderer/store/runtime'
|
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { Assistant, Message, Topic } from '@renderer/types'
|
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
||||||
import { delay, uuid } from '@renderer/utils'
|
import { Assistant, FileType, Message, Topic } from '@renderer/types'
|
||||||
import { Button, Divider, Popconfirm, Tag, Tooltip } from 'antd'
|
import { delay, getFileExtension, uuid } from '@renderer/utils'
|
||||||
|
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { debounce, isEmpty } from 'lodash'
|
import { debounce, isEmpty } from 'lodash'
|
||||||
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import AttachmentButton from './AttachmentButton'
|
||||||
|
import AttachmentPreview from './AttachmentPreview'
|
||||||
import SendMessageButton from './SendMessageButton'
|
import SendMessageButton from './SendMessageButton'
|
||||||
|
import TokenCount from './TokenCount'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
@@ -38,17 +45,26 @@ let _text = ''
|
|||||||
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||||
const [text, setText] = useState(_text)
|
const [text, setText] = useState(_text)
|
||||||
const [inputFocus, setInputFocus] = useState(false)
|
const [inputFocus, setInputFocus] = useState(false)
|
||||||
const { addTopic } = useAssistant(assistant.id)
|
const { addTopic, model } = useAssistant(assistant.id)
|
||||||
const { sendMessageShortcut, showInputEstimatedTokens, fontSize } = useSettings()
|
const { sendMessageShortcut, fontSize, pasteLongTextAsFile } = useSettings()
|
||||||
const [expended, setExpend] = useState(false)
|
const [expended, setExpend] = useState(false)
|
||||||
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
||||||
|
const [contextCount, setContextCount] = useState(0)
|
||||||
const generating = useAppSelector((state) => state.runtime.generating)
|
const generating = useAppSelector((state) => state.runtime.generating)
|
||||||
const inputRef = useRef<TextAreaRef>(null)
|
const textareaRef = useRef<TextAreaRef>(null)
|
||||||
|
const [files, setFiles] = useState<FileType[]>([])
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const containerRef = useRef(null)
|
||||||
|
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||||
|
const { searching } = useRuntime()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||||
|
const supportExts = useMemo(() => [...textExts, ...(isVision ? imageExts : [])], [isVision])
|
||||||
|
|
||||||
_text = text
|
_text = text
|
||||||
|
|
||||||
const sendMessage = useCallback(() => {
|
const sendMessage = useCallback(async () => {
|
||||||
if (generating) {
|
if (generating) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -67,24 +83,32 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
status: 'success'
|
status: 'success'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (files.length > 0) {
|
||||||
|
message.files = await FileManager.uploadFiles(files)
|
||||||
|
}
|
||||||
|
|
||||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||||
|
|
||||||
setText('')
|
setText('')
|
||||||
|
setFiles([])
|
||||||
setTimeout(() => setText(''), 500)
|
setTimeout(() => setText(''), 500)
|
||||||
|
setTimeout(() => resizeTextArea(), 0)
|
||||||
|
|
||||||
setExpend(false)
|
setExpend(false)
|
||||||
}, [assistant.id, assistant.topics, generating, text])
|
}, [assistant.id, assistant.topics, generating, files, text])
|
||||||
|
|
||||||
const inputTokenCount = useMemo(() => estimateInputTokenCount(text), [text])
|
const inputTokenCount = useMemo(() => estimateTextTokens(text), [text])
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
const isEnterPressed = event.keyCode == 13
|
||||||
|
|
||||||
if (expended) {
|
if (expended) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
return setExpend(false)
|
return setExpend(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendMessageShortcut === 'Enter' && event.key === 'Enter') {
|
if (sendMessageShortcut === 'Enter' && isEnterPressed) {
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -92,7 +116,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
return event.preventDefault()
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendMessageShortcut === 'Shift+Enter' && event.key === 'Enter' && event.shiftKey) {
|
if (sendMessageShortcut === 'Shift+Enter' && isEnterPressed && event.shiftKey) {
|
||||||
sendMessage()
|
sendMessage()
|
||||||
return event.preventDefault()
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
@@ -102,6 +126,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
const topic = getDefaultTopic()
|
const topic = getDefaultTopic()
|
||||||
addTopic(topic)
|
addTopic(topic)
|
||||||
setActiveTopic(topic)
|
setActiveTopic(topic)
|
||||||
|
db.topics.add({ id: topic.id, messages: [] })
|
||||||
}, [addTopic, setActiveTopic])
|
}, [addTopic, setActiveTopic])
|
||||||
|
|
||||||
const clearTopic = async () => {
|
const clearTopic = async () => {
|
||||||
@@ -117,6 +142,87 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
store.dispatch(setGenerating(false))
|
store.dispatch(setGenerating(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onNewContext = () => {
|
||||||
|
if (generating) {
|
||||||
|
onPause()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeTextArea = () => {
|
||||||
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
|
if (textArea) {
|
||||||
|
textArea.style.height = 'auto'
|
||||||
|
textArea.style.height = textArea?.scrollHeight > 400 ? '400px' : `${textArea?.scrollHeight}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onToggleExpended = () => {
|
||||||
|
const isExpended = !expended
|
||||||
|
setExpend(isExpended)
|
||||||
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
|
|
||||||
|
if (textArea) {
|
||||||
|
if (isExpended) {
|
||||||
|
textArea.style.height = '70vh'
|
||||||
|
} else {
|
||||||
|
resizeTextArea()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textareaRef.current?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInput = () => !expended && resizeTextArea()
|
||||||
|
|
||||||
|
const onPaste = useCallback(
|
||||||
|
async (event: ClipboardEvent) => {
|
||||||
|
for (const file of event.clipboardData?.files || []) {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
if (file.path === '') {
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
const tempFilePath = await window.api.file.create(file.name)
|
||||||
|
const arrayBuffer = await file.arrayBuffer()
|
||||||
|
const uint8Array = new Uint8Array(arrayBuffer)
|
||||||
|
await window.api.file.write(tempFilePath, uint8Array)
|
||||||
|
const selectedFile = await window.api.file.get(tempFilePath)
|
||||||
|
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.path) {
|
||||||
|
if (supportExts.includes(getFileExtension(file.path))) {
|
||||||
|
const selectedFile = await window.api.file.get(file.path)
|
||||||
|
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pasteLongTextAsFile) {
|
||||||
|
const item = event.clipboardData?.items[0]
|
||||||
|
if (item && item.kind === 'string' && item.type === 'text/plain') {
|
||||||
|
event.preventDefault()
|
||||||
|
item.getAsString(async (text) => {
|
||||||
|
if (text.length > 1500) {
|
||||||
|
console.debug(item.getAsFile())
|
||||||
|
const tempFilePath = await window.api.file.create('pasted_text.txt')
|
||||||
|
await window.api.file.write(tempFilePath, text)
|
||||||
|
const selectedFile = await window.api.file.get(tempFilePath)
|
||||||
|
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||||
|
} else {
|
||||||
|
setText((prevText) => prevText + text)
|
||||||
|
setTimeout(() => resizeTextArea(), 0)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[supportExts, pasteLongTextAsFile]
|
||||||
|
)
|
||||||
|
|
||||||
// Command or Ctrl + N create new topic
|
// Command or Ctrl + N create new topic
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeydown = (e) => {
|
const onKeydown = (e) => {
|
||||||
@@ -124,7 +230,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
|
||||||
addNewTopic()
|
addNewTopic()
|
||||||
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||||
inputRef.current?.focus()
|
textareaRef.current?.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,104 +243,114 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => {
|
EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => {
|
||||||
setText(message.content)
|
setText(message.content)
|
||||||
inputRef.current?.focus()
|
textareaRef.current?.focus()
|
||||||
|
setTimeout(() => resizeTextArea(), 0)
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, _setEstimateTokenCount)
|
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, ({ tokensCount, contextCount }) => {
|
||||||
|
_setEstimateTokenCount(tokensCount)
|
||||||
|
setContextCount(contextCount)
|
||||||
|
})
|
||||||
]
|
]
|
||||||
return () => unsubscribes.forEach((unsub) => unsub())
|
return () => unsubscribes.forEach((unsub) => unsub())
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
inputRef.current?.focus()
|
textareaRef.current?.focus()
|
||||||
}, [assistant])
|
}, [assistant])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('paste', onPaste)
|
||||||
|
return () => document.removeEventListener('paste', onPaste)
|
||||||
|
}, [onPaste])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container>
|
||||||
id="inputbar"
|
<AttachmentPreview files={files} setFiles={setFiles} />
|
||||||
style={{ minHeight: expended ? '60%' : 'var(--input-bar-height)' }}
|
<InputBarContainer id="inputbar" className={inputFocus ? 'focus' : ''} ref={containerRef}>
|
||||||
className={inputFocus ? 'focus' : ''}>
|
<Textarea
|
||||||
<Toolbar>
|
value={text}
|
||||||
<ToolbarMenu>
|
onChange={(e) => setText(e.target.value)}
|
||||||
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
|
onKeyDown={handleKeyDown}
|
||||||
<ToolbarButton type="text" onClick={addNewTopic}>
|
placeholder={t('chat.input.placeholder')}
|
||||||
<PlusCircleOutlined />
|
autoFocus
|
||||||
</ToolbarButton>
|
contextMenu="true"
|
||||||
</Tooltip>
|
variant="borderless"
|
||||||
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
|
rows={1}
|
||||||
<Popconfirm
|
ref={textareaRef}
|
||||||
title={t('chat.input.clear.content')}
|
style={{ fontSize }}
|
||||||
placement="top"
|
styles={{ textarea: TextareaStyle }}
|
||||||
onConfirm={clearTopic}
|
onFocus={() => setInputFocus(true)}
|
||||||
okButtonProps={{ danger: true }}
|
onBlur={() => setInputFocus(false)}
|
||||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
onInput={onInput}
|
||||||
okText={t('chat.input.clear')}>
|
disabled={searching}
|
||||||
<ToolbarButton type="text">
|
onClick={() => searching && dispatch(setSearching(false))}
|
||||||
<ClearOutlined />
|
/>
|
||||||
</ToolbarButton>
|
<Toolbar>
|
||||||
</Popconfirm>
|
<ToolbarMenu>
|
||||||
</Tooltip>
|
<Tooltip placement="top" title={t('chat.input.new_topic')} arrow>
|
||||||
<Tooltip placement="top" title={t('chat.input.topics')} arrow>
|
<ToolbarButton type="text" onClick={addNewTopic}>
|
||||||
<ToolbarButton type="text" onClick={() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)}>
|
<FormOutlined />
|
||||||
<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={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
|
||||||
<ToolbarButton type="text" onClick={() => setExpend(!expended)}>
|
|
||||||
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
|
||||||
</ToolbarButton>
|
|
||||||
</Tooltip>
|
|
||||||
{showInputEstimatedTokens && (
|
|
||||||
<TextCount>
|
|
||||||
<Tooltip title={t('chat.input.context_count.tip') + ' | ' + t('chat.input.estimated_tokens.tip')}>
|
|
||||||
<Tag
|
|
||||||
style={{
|
|
||||||
cursor: 'pointer',
|
|
||||||
borderRadius: '6px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
padding: '2px 8px',
|
|
||||||
borderWidth: 0.5
|
|
||||||
}}>
|
|
||||||
<i className="iconfont icon-history" style={{ marginRight: '3px' }} />
|
|
||||||
{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}
|
|
||||||
<Divider type="vertical" style={{ marginTop: 2, marginLeft: 5, marginRight: 5 }} />↑{inputTokenCount}
|
|
||||||
<span style={{ margin: '0 2px' }}>/</span>
|
|
||||||
{estimateTokenCount}
|
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
</TextCount>
|
|
||||||
)}
|
|
||||||
</ToolbarMenu>
|
|
||||||
<ToolbarMenu>
|
|
||||||
{generating && (
|
|
||||||
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
|
||||||
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
|
|
||||||
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
|
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
|
||||||
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />}
|
<Popconfirm
|
||||||
</ToolbarMenu>
|
title={t('chat.input.clear.content')}
|
||||||
</Toolbar>
|
placement="top"
|
||||||
<Textarea
|
onConfirm={clearTopic}
|
||||||
value={text}
|
okButtonProps={{ danger: true }}
|
||||||
onChange={(e) => setText(e.target.value)}
|
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||||
onKeyDown={handleKeyDown}
|
okText={t('chat.input.clear')}>
|
||||||
placeholder={t('chat.input.placeholder')}
|
<ToolbarButton type="text">
|
||||||
autoFocus
|
<ClearOutlined />
|
||||||
contextMenu="true"
|
</ToolbarButton>
|
||||||
variant="borderless"
|
</Popconfirm>
|
||||||
ref={inputRef}
|
</Tooltip>
|
||||||
styles={{ textarea: { paddingLeft: 0 } }}
|
<Tooltip placement="top" title={t('chat.input.topics')} arrow>
|
||||||
onFocus={() => setInputFocus(true)}
|
<ToolbarButton
|
||||||
onBlur={() => setInputFocus(false)}
|
type="text"
|
||||||
style={{ fontSize }}
|
onClick={() => {
|
||||||
/>
|
!showTopics && toggleShowTopics()
|
||||||
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
||||||
|
}}>
|
||||||
|
<HistoryOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
|
||||||
|
<ToolbarButton
|
||||||
|
type="text"
|
||||||
|
onClick={() => {
|
||||||
|
!showTopics && toggleShowTopics()
|
||||||
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
|
||||||
|
}}>
|
||||||
|
<ControlOutlined />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
|
||||||
|
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||||
|
<ToolbarButton type="text" onClick={onToggleExpended}>
|
||||||
|
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
<TokenCount
|
||||||
|
estimateTokenCount={estimateTokenCount}
|
||||||
|
inputTokenCount={inputTokenCount}
|
||||||
|
contextCount={contextCount}
|
||||||
|
ToolbarButton={ToolbarButton}
|
||||||
|
onClick={onNewContext}
|
||||||
|
/>
|
||||||
|
</ToolbarMenu>
|
||||||
|
<ToolbarMenu>
|
||||||
|
{generating && (
|
||||||
|
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
||||||
|
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
|
||||||
|
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || !text} />}
|
||||||
|
</ToolbarMenu>
|
||||||
|
</Toolbar>
|
||||||
|
</InputBarContainer>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -242,7 +358,14 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
|||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: var(--input-bar-height);
|
`
|
||||||
|
|
||||||
|
const TextareaStyle: CSSProperties = {
|
||||||
|
paddingLeft: 0,
|
||||||
|
padding: '10px 15px 8px'
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputBarContainer = styled.div`
|
||||||
border: 1px solid var(--color-border-soft);
|
border: 1px solid var(--color-border-soft);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -257,11 +380,11 @@ const Textarea = styled(TextArea)`
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin: 0 15px 5px 15px;
|
|
||||||
font-family: Ubuntu;
|
font-family: Ubuntu;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
width: auto;
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Toolbar = styled.div`
|
const Toolbar = styled.div`
|
||||||
@@ -269,8 +392,9 @@ const Toolbar = styled.div`
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
padding-top: 3px;
|
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
height: 36px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const ToolbarMenu = styled.div`
|
const ToolbarMenu = styled.div`
|
||||||
@@ -281,31 +405,39 @@ const ToolbarMenu = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const ToolbarButton = styled(Button)`
|
const ToolbarButton = styled(Button)`
|
||||||
width: 32px;
|
width: 30px;
|
||||||
height: 32px;
|
height: 30px;
|
||||||
font-size: 18px;
|
font-size: 17px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
color: var(--color-icon);
|
color: var(--color-icon);
|
||||||
&.anticon {
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
&.anticon,
|
||||||
|
&.iconfont {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
color: var(--color-icon);
|
color: var(--color-icon);
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--color-background-soft);
|
background-color: var(--color-background-soft);
|
||||||
.anticon {
|
.anticon,
|
||||||
|
.iconfont {
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-primary) !important;
|
||||||
|
.anticon,
|
||||||
|
.iconfont {
|
||||||
|
color: var(--color-white-soft);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const TextCount = styled.div`
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--color-text-3);
|
|
||||||
z-index: 10;
|
|
||||||
padding: 2px;
|
|
||||||
border-top-left-radius: 7px;
|
|
||||||
user-select: none;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default Inputbar
|
export default Inputbar
|
||||||
|
|||||||
83
src/renderer/src/pages/home/Inputbar/TokenCount.tsx
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { ArrowUpOutlined, MenuOutlined, PicCenterOutlined } from '@ant-design/icons'
|
||||||
|
import { HStack, VStack } from '@renderer/components/Layout'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { Divider, Popover, Tooltip } from 'antd'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
estimateTokenCount: number
|
||||||
|
inputTokenCount: number
|
||||||
|
contextCount: number
|
||||||
|
ToolbarButton: any
|
||||||
|
} & React.HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
|
const TokenCount: FC<Props> = ({ estimateTokenCount, inputTokenCount, contextCount, ToolbarButton, ...props }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { showInputEstimatedTokens } = useSettings()
|
||||||
|
|
||||||
|
if (!showInputEstimatedTokens) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopoverContent = () => {
|
||||||
|
return (
|
||||||
|
<VStack w="150px" background="100%">
|
||||||
|
<HStack justifyContent="space-between" w="100%">
|
||||||
|
<Text>{t('chat.input.context_count.tip')}</Text>
|
||||||
|
<Text>{contextCount}</Text>
|
||||||
|
</HStack>
|
||||||
|
<Divider style={{ margin: '5px 0' }} />
|
||||||
|
<HStack justifyContent="space-between" w="100%">
|
||||||
|
<Text>{t('chat.input.estimated_tokens.tip')}</Text>
|
||||||
|
<Text>{estimateTokenCount}</Text>
|
||||||
|
</HStack>
|
||||||
|
</VStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ToolbarButton type="text" onClick={props.onClick}>
|
||||||
|
<Tooltip placement="top" title={t('chat.input.new.context')}>
|
||||||
|
<PicCenterOutlined />
|
||||||
|
</Tooltip>
|
||||||
|
</ToolbarButton>
|
||||||
|
<Container>
|
||||||
|
<Popover content={PopoverContent}>
|
||||||
|
<MenuOutlined /> {contextCount}
|
||||||
|
<Divider type="vertical" style={{ marginTop: 0, marginLeft: 5, marginRight: 5 }} />
|
||||||
|
<ArrowUpOutlined />
|
||||||
|
{inputTokenCount} / {estimateTokenCount}
|
||||||
|
</Popover>
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
z-index: 10;
|
||||||
|
padding: 3px 10px;
|
||||||
|
user-select: none;
|
||||||
|
font-family: Ubuntu;
|
||||||
|
border: 0.5px solid var(--color-text-3);
|
||||||
|
border-radius: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
.anticon {
|
||||||
|
font-size: 10px;
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Text = styled.div`
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
`
|
||||||
|
|
||||||
|
export default TokenCount
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { CheckOutlined } from '@ant-design/icons'
|
import { CheckOutlined } from '@ant-design/icons'
|
||||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { initMermaid } from '@renderer/init'
|
import { initMermaid } from '@renderer/init'
|
||||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
import { ThemeMode } from '@renderer/types'
|
||||||
import { ThemeMode } from '@renderer/store/settings'
|
import React, { memo, useState } from 'react'
|
||||||
import React, { useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
import { atomDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
import { atomDark, oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||||
@@ -17,45 +17,60 @@ interface CodeBlockProps {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) => {
|
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||||
const match = /language-(\w+)/.exec(className || '')
|
const match = /language-(\w+)/.exec(className || '')
|
||||||
const [copied, setCopied] = useState(false)
|
const showFooterCopyButton = children && children.length > 500
|
||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const onCopy = () => {
|
|
||||||
navigator.clipboard.writeText(children)
|
|
||||||
window.message.success({ content: t('message.copied'), key: 'copy-code' })
|
|
||||||
setCopied(true)
|
|
||||||
setTimeout(() => setCopied(false), 2000)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (match && match[1] === 'mermaid') {
|
if (match && match[1] === 'mermaid') {
|
||||||
initMermaid(theme)
|
initMermaid(theme)
|
||||||
return <Mermaid chart={children} />
|
return <Mermaid chart={children} />
|
||||||
}
|
}
|
||||||
|
|
||||||
return match ? (
|
return match ? (
|
||||||
<div>
|
<div className="code-block">
|
||||||
<CodeHeader>
|
<CodeHeader>
|
||||||
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
||||||
{!copied && <CopyIcon className="copy" onClick={onCopy} />}
|
<CopyButton text={children} />
|
||||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
|
||||||
</CodeHeader>
|
</CodeHeader>
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
{...rest}
|
|
||||||
language={match[1]}
|
language={match[1]}
|
||||||
style={theme === ThemeMode.dark ? atomDark : oneLight}
|
style={theme === ThemeMode.dark ? atomDark : oneLight}
|
||||||
wrapLongLines={true}
|
wrapLongLines={true}
|
||||||
customStyle={{ borderTopLeftRadius: 0, borderTopRightRadius: 0, marginTop: 0 }}>
|
customStyle={{
|
||||||
|
border: '0.5px solid var(--color-code-background)',
|
||||||
|
borderTopLeftRadius: 0,
|
||||||
|
borderTopRightRadius: 0,
|
||||||
|
marginTop: 0
|
||||||
|
}}>
|
||||||
{String(children).replace(/\n$/, '')}
|
{String(children).replace(/\n$/, '')}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
|
{showFooterCopyButton && (
|
||||||
|
<CodeFooter>
|
||||||
|
<CopyButton text={children} style={{ marginTop: -40, marginRight: 10 }} />
|
||||||
|
</CodeFooter>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<code {...rest} className={className}>
|
<code className={className}>{children}</code>
|
||||||
{children}
|
)
|
||||||
</code>
|
}
|
||||||
|
|
||||||
|
const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ text, style }) => {
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const onCopy = () => {
|
||||||
|
navigator.clipboard.writeText(text)
|
||||||
|
window.message.success({ content: t('message.copied'), key: 'copy-code' })
|
||||||
|
setCopied(true)
|
||||||
|
setTimeout(() => setCopied(false), 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return copied ? (
|
||||||
|
<CheckOutlined style={{ color: 'var(--color-primary)', ...style }} />
|
||||||
|
) : (
|
||||||
|
<CopyIcon className="copy" style={style} onClick={onCopy} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +82,7 @@ const CodeHeader = styled.div`
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background-color: var(--color-code-background);
|
background-color: var(--color-code-background);
|
||||||
height: 40px;
|
height: 36px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
border-top-left-radius: 8px;
|
border-top-left-radius: 8px;
|
||||||
border-top-right-radius: 8px;
|
border-top-right-radius: 8px;
|
||||||
@@ -85,4 +100,19 @@ const CodeLanguage = styled.div`
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default CodeBlock
|
const CodeFooter = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
.copy {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
transition: color 0.3s;
|
||||||
|
}
|
||||||
|
.copy:hover {
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default memo(CodeBlock)
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import 'katex/dist/katex.min.css'
|
import 'katex/dist/katex.min.css'
|
||||||
|
|
||||||
import { Message } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
import { convertMathFormula } from '@renderer/utils'
|
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { FC, useMemo } from 'react'
|
import { FC, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown, { Components } from 'react-markdown'
|
||||||
import rehypeKatex from 'rehype-katex'
|
import rehypeKatex from 'rehype-katex'
|
||||||
import remarkGfm from 'remark-gfm'
|
import remarkGfm from 'remark-gfm'
|
||||||
import remarkMath from 'remark-math'
|
import remarkMath from 'remark-math'
|
||||||
@@ -17,6 +16,14 @@ interface Props {
|
|||||||
message: Message
|
message: Message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rehypePlugins = [rehypeKatex]
|
||||||
|
const remarkPlugins = [remarkMath, remarkGfm]
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
code: CodeBlock,
|
||||||
|
a: Link
|
||||||
|
}
|
||||||
|
|
||||||
const Markdown: FC<Props> = ({ message }) => {
|
const Markdown: FC<Props> = ({ message }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -24,25 +31,54 @@ const Markdown: FC<Props> = ({ message }) => {
|
|||||||
const empty = isEmpty(message.content)
|
const empty = isEmpty(message.content)
|
||||||
const paused = message.status === 'paused'
|
const paused = message.status === 'paused'
|
||||||
const content = empty && paused ? t('message.chat.completion.paused') : message.content
|
const content = empty && paused ? t('message.chat.completion.paused') : message.content
|
||||||
return convertMathFormula(content)
|
return escapeBrackets(escapeDollarNumber(content))
|
||||||
}, [message.content, message.status, t])
|
}, [message.content, message.status, t])
|
||||||
|
|
||||||
return useMemo(() => {
|
return (
|
||||||
return (
|
<ReactMarkdown
|
||||||
<ReactMarkdown
|
className="markdown"
|
||||||
className="markdown"
|
rehypePlugins={rehypePlugins}
|
||||||
rehypePlugins={[rehypeKatex]}
|
remarkPlugins={remarkPlugins}
|
||||||
remarkPlugins={[[remarkMath, { singleDollarTextMath: false }], remarkGfm]}
|
components={components as Partial<Components>}
|
||||||
remarkRehypeOptions={{
|
remarkRehypeOptions={{
|
||||||
footnoteLabel: t('common.footnotes'),
|
footnoteLabel: t('common.footnotes'),
|
||||||
footnoteLabelTagName: 'h4',
|
footnoteLabelTagName: 'h4',
|
||||||
footnoteBackContent: ' '
|
footnoteBackContent: ' '
|
||||||
}}
|
}}>
|
||||||
components={{ code: CodeBlock as any, a: Link as any }}>
|
{messageContent}
|
||||||
{messageContent}
|
</ReactMarkdown>
|
||||||
</ReactMarkdown>
|
)
|
||||||
)
|
}
|
||||||
}, [messageContent, t])
|
|
||||||
|
function escapeDollarNumber(text: string) {
|
||||||
|
let escapedText = ''
|
||||||
|
|
||||||
|
for (let i = 0; i < text.length; i += 1) {
|
||||||
|
let char = text[i]
|
||||||
|
const nextChar = text[i + 1] || ' '
|
||||||
|
|
||||||
|
if (char === '$' && nextChar >= '0' && nextChar <= '9') {
|
||||||
|
char = '\\$'
|
||||||
|
}
|
||||||
|
|
||||||
|
escapedText += char
|
||||||
|
}
|
||||||
|
|
||||||
|
return escapedText
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeBrackets(text: string) {
|
||||||
|
const pattern = /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g
|
||||||
|
return text.replace(pattern, (match, codeBlock, squareBracket, roundBracket) => {
|
||||||
|
if (codeBlock) {
|
||||||
|
return codeBlock
|
||||||
|
} else if (squareBracket) {
|
||||||
|
return `$$${squareBracket}$$`
|
||||||
|
} else if (roundBracket) {
|
||||||
|
return `$${roundBracket}$`
|
||||||
|
}
|
||||||
|
return match
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Markdown
|
export default Markdown
|
||||||
|
|||||||
@@ -2,21 +2,25 @@ import {
|
|||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
|
ForkOutlined,
|
||||||
MenuOutlined,
|
MenuOutlined,
|
||||||
QuestionCircleOutlined,
|
QuestionCircleOutlined,
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
SyncOutlined
|
SyncOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
|
import UserPopup from '@renderer/components/Popups/UserPopup'
|
||||||
import { FONT_FAMILY } from '@renderer/config/constant'
|
import { FONT_FAMILY } from '@renderer/config/constant'
|
||||||
|
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
|
||||||
|
import { startMinAppById } from '@renderer/config/minapp'
|
||||||
import { getModelLogo } from '@renderer/config/provider'
|
import { getModelLogo } from '@renderer/config/provider'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
import useAvatar from '@renderer/hooks/useAvatar'
|
||||||
|
import { useModel } from '@renderer/hooks/useModel'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useRuntime } from '@renderer/hooks/useStore'
|
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
import { Message, Model } from '@renderer/types'
|
import { Message, Model } from '@renderer/types'
|
||||||
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
|
import { firstLetter, removeLeadingEmoji, removeTrailingDoubleSpaces } from '@renderer/utils'
|
||||||
import { Alert, Avatar, Dropdown, Popconfirm, Tooltip } from 'antd'
|
import { Alert, Avatar, Divider, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { upperFirst } from 'lodash'
|
import { upperFirst } from 'lodash'
|
||||||
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
import { FC, memo, useCallback, useMemo, useState } from 'react'
|
||||||
@@ -25,6 +29,8 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
import SelectModelDropdown from '../components/SelectModelDropdown'
|
import SelectModelDropdown from '../components/SelectModelDropdown'
|
||||||
import Markdown from '../Markdown/Markdown'
|
import Markdown from '../Markdown/Markdown'
|
||||||
|
import MessageAttachments from './MessageAttachments'
|
||||||
|
import MessgeTokens from './MessageTokens'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
@@ -37,19 +43,18 @@ interface Props {
|
|||||||
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
|
const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) => {
|
||||||
const avatar = useAvatar()
|
const avatar = useAvatar()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { assistant, model, setModel } = useAssistant(message.assistantId)
|
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||||
|
const model = useModel(message.modelId)
|
||||||
const { userName, showMessageDivider, messageFont, fontSize } = useSettings()
|
const { userName, showMessageDivider, messageFont, fontSize } = useSettings()
|
||||||
const { generating } = useRuntime()
|
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
const isLastMessage = index === 0
|
const isLastMessage = index === 0
|
||||||
const isUserMessage = message.role === 'user'
|
const isUserMessage = message.role === 'user'
|
||||||
const isAssistantMessage = message.role === 'assistant'
|
const isAssistantMessage = message.role === 'assistant'
|
||||||
const canRegenerate = isLastMessage && isAssistantMessage
|
const canRegenerate = isLastMessage && isAssistantMessage
|
||||||
const showMetadata = Boolean(message.usage) && !generating
|
|
||||||
|
|
||||||
const onCopy = useCallback(() => {
|
const onCopy = useCallback(() => {
|
||||||
navigator.clipboard.writeText(message.content)
|
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
|
||||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
@@ -65,19 +70,29 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
[setModel]
|
[setModel]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onNewBranch = useCallback(() => {
|
||||||
|
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
|
||||||
|
}, [index])
|
||||||
|
|
||||||
const getUserName = useCallback(() => {
|
const getUserName = useCallback(() => {
|
||||||
if (message.id === 'assistant') return assistant?.name
|
if (isLocalAi && message.role !== 'user') return APP_NAME
|
||||||
if (message.role === 'assistant') return upperFirst(model.name || model.id)
|
if (message.role === 'assistant') return upperFirst(model?.name || model?.id)
|
||||||
return userName || t('common.you')
|
return userName || t('common.you')
|
||||||
}, [assistant?.name, message.id, message.role, model.id, model.name, t, userName])
|
}, [message.role, model?.id, model?.name, t, userName])
|
||||||
|
|
||||||
const fontFamily = useMemo(() => {
|
const fontFamily = useMemo(() => {
|
||||||
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
|
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
|
||||||
}, [messageFont])
|
}, [messageFont])
|
||||||
|
|
||||||
const messageBorder = showMessageDivider ? undefined : 'none'
|
const messageBorder = showMessageDivider ? undefined : 'none'
|
||||||
const avatarSource = useMemo(() => (message.modelId ? getModelLogo(message.modelId) : undefined), [message.modelId])
|
|
||||||
|
const avatarSource = useMemo(() => {
|
||||||
|
if (isLocalAi) return AppLogo
|
||||||
|
return message.modelId ? getModelLogo(message.modelId) : undefined
|
||||||
|
}, [message.modelId])
|
||||||
|
|
||||||
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
|
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
|
||||||
|
|
||||||
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
|
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
|
||||||
|
|
||||||
const dropdownItems = useMemo(
|
const dropdownItems = useMemo(
|
||||||
@@ -95,39 +110,39 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
[t, message]
|
[t, message]
|
||||||
)
|
)
|
||||||
|
|
||||||
const MessageItem = useCallback(() => {
|
const showMiniApp = () => model?.provider && startMinAppById(model?.provider)
|
||||||
if (message.status === 'sending') {
|
|
||||||
return (
|
|
||||||
<MessageContentLoading>
|
|
||||||
<SyncOutlined spin size={24} />
|
|
||||||
</MessageContentLoading>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (message.status === 'error') {
|
if (message.type === 'clear') {
|
||||||
return (
|
return (
|
||||||
<Alert
|
<Divider dashed style={{ padding: '0 20px' }} plain>
|
||||||
message={t('error.chat.response')}
|
{t('chat.message.new.context')}
|
||||||
description={<Markdown message={message} />}
|
</Divider>
|
||||||
type="error"
|
)
|
||||||
style={{ marginBottom: 15 }}
|
}
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Markdown message={message} />
|
|
||||||
}, [message])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageContainer key={message.id} className="message">
|
<MessageContainer key={message.id} className="message">
|
||||||
<MessageHeader>
|
<MessageHeader>
|
||||||
<AvatarWrapper>
|
<AvatarWrapper>
|
||||||
{isAssistantMessage ? (
|
{isAssistantMessage ? (
|
||||||
<Avatar src={avatarSource} size={35} style={{ borderRadius: '20%' }}>
|
<Avatar
|
||||||
|
src={avatarSource}
|
||||||
|
size={35}
|
||||||
|
style={{
|
||||||
|
borderRadius: '20%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: '1px solid var(--color-border)'
|
||||||
|
}}
|
||||||
|
onClick={showMiniApp}>
|
||||||
{avatarName}
|
{avatarName}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
) : (
|
) : (
|
||||||
<Avatar src={avatar} size={35} style={{ borderRadius: '20%' }} />
|
<Avatar
|
||||||
|
src={avatar}
|
||||||
|
size={35}
|
||||||
|
style={{ borderRadius: '20%', cursor: 'pointer' }}
|
||||||
|
onClick={() => UserPopup.show()}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<UserWrap>
|
<UserWrap>
|
||||||
<UserName>{username}</UserName>
|
<UserName>{username}</UserName>
|
||||||
@@ -135,11 +150,12 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
</UserWrap>
|
</UserWrap>
|
||||||
</AvatarWrapper>
|
</AvatarWrapper>
|
||||||
</MessageHeader>
|
</MessageHeader>
|
||||||
<MessageContent style={{ fontFamily, fontSize }}>
|
<MessageContentContainer style={{ fontFamily, fontSize }}>
|
||||||
<MessageItem />
|
<MessageContent message={message} />
|
||||||
<MessageFooter style={{ border: messageBorder }}>
|
<MessageFooter style={{ border: messageBorder, flexDirection: isLastMessage ? 'row-reverse' : undefined }}>
|
||||||
|
<MessgeTokens message={message} />
|
||||||
{showMenu && (
|
{showMenu && (
|
||||||
<MenusBar className={`menubar ${isLastMessage && 'show'} ${(!isLastMessage || isUserMessage) && 'user'}`}>
|
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||||
{message.role === 'user' && (
|
{message.role === 'user' && (
|
||||||
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
||||||
<ActionButton onClick={onEdit}>
|
<ActionButton onClick={onEdit}>
|
||||||
@@ -154,7 +170,7 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{canRegenerate && (
|
{canRegenerate && (
|
||||||
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topRight">
|
<SelectModelDropdown model={model} onSelect={onRegenerate} placement="topLeft">
|
||||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||||
<ActionButton>
|
<ActionButton>
|
||||||
<SyncOutlined />
|
<SyncOutlined />
|
||||||
@@ -162,6 +178,13 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</SelectModelDropdown>
|
</SelectModelDropdown>
|
||||||
)}
|
)}
|
||||||
|
{isAssistantMessage && (
|
||||||
|
<Tooltip title={t('chat.message.new.branch')} mouseEnterDelay={0.8}>
|
||||||
|
<ActionButton onClick={onNewBranch}>
|
||||||
|
<ForkOutlined />
|
||||||
|
</ActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t('message.message.delete.content')}
|
title={t('message.message.delete.content')}
|
||||||
okButtonProps={{ danger: true }}
|
okButtonProps={{ danger: true }}
|
||||||
@@ -182,18 +205,42 @@ const MessageItem: FC<Props> = ({ message, index, showMenu, onDeleteMessage }) =
|
|||||||
)}
|
)}
|
||||||
</MenusBar>
|
</MenusBar>
|
||||||
)}
|
)}
|
||||||
{showMetadata && (
|
|
||||||
<MessageMetadata>
|
|
||||||
Tokens: {message?.usage?.total_tokens} | ↑{message?.usage?.prompt_tokens} | ↓
|
|
||||||
{message?.usage?.completion_tokens}
|
|
||||||
</MessageMetadata>
|
|
||||||
)}
|
|
||||||
</MessageFooter>
|
</MessageFooter>
|
||||||
</MessageContent>
|
</MessageContentContainer>
|
||||||
</MessageContainer>
|
</MessageContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MessageContent: React.FC<{ message: Message }> = ({ message }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
if (message.status === 'sending') {
|
||||||
|
return (
|
||||||
|
<MessageContentLoading>
|
||||||
|
<SyncOutlined spin size={24} />
|
||||||
|
</MessageContentLoading>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.status === 'error') {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
message={<div style={{ fontSize: 14 }}>{t('error.chat.response')}</div>}
|
||||||
|
description={<Markdown message={message} />}
|
||||||
|
type="error"
|
||||||
|
style={{ marginBottom: 15, padding: 10, fontSize: 12 }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Markdown message={message} />
|
||||||
|
<MessageAttachments message={message} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const MessageContainer = styled.div`
|
const MessageContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -250,7 +297,7 @@ const MessageTime = styled.div`
|
|||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
`
|
`
|
||||||
|
|
||||||
const MessageContent = styled.div`
|
const MessageContentContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -285,13 +332,6 @@ const MenusBar = styled.div`
|
|||||||
margin-left: -5px;
|
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`
|
const ActionButton = styled.div`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
48
src/renderer/src/pages/home/Messages/MessageAttachments.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { FileTypes, Message } from '@renderer/types'
|
||||||
|
import { getFileDirectory } from '@renderer/utils'
|
||||||
|
import { Image as AntdImage, Upload } from 'antd'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
message: Message
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageAttachments: FC<Props> = ({ message }) => {
|
||||||
|
if (message?.files && message.files[0]?.type === FileTypes.IMAGE) {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
{message.files?.map((image) => <Image src={'file://' + image.path} key={image.id} width="33%" />)}
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container style={{ marginTop: 2, marginBottom: 8 }}>
|
||||||
|
<Upload
|
||||||
|
listType="picture"
|
||||||
|
disabled
|
||||||
|
onPreview={(item) => item.url && window.open(getFileDirectory(item.url))}
|
||||||
|
fileList={message.files?.map((file) => ({
|
||||||
|
uid: file.id,
|
||||||
|
url: 'file://' + file.path,
|
||||||
|
status: 'done',
|
||||||
|
name: file.origin_name
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Image = styled(AntdImage)`
|
||||||
|
border-radius: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default MessageAttachments
|
||||||
38
src/renderer/src/pages/home/Messages/MessageTokens.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { useRuntime } from '@renderer/hooks/useStore'
|
||||||
|
import { Message } from '@renderer/types'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
const MessgeTokens: React.FC<{ message: Message }> = ({ message }) => {
|
||||||
|
const { generating } = useRuntime()
|
||||||
|
|
||||||
|
if (!message.usage) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.role === 'user') {
|
||||||
|
return <MessageMetadata>Tokens: {message?.usage?.total_tokens}</MessageMetadata>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (generating) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.role === 'assistant') {
|
||||||
|
return (
|
||||||
|
<MessageMetadata>
|
||||||
|
Tokens: {message?.usage?.total_tokens} | ↑{message?.usage?.prompt_tokens} | ↓{message?.usage?.completion_tokens}
|
||||||
|
</MessageMetadata>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const MessageMetadata = styled.div`
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
user-select: text;
|
||||||
|
margin: 2px 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default MessgeTokens
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
|
import db from '@renderer/databases'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useProviderByAssistant } from '@renderer/hooks/useProvider'
|
import { getTopic, TopicManager } from '@renderer/hooks/useTopic'
|
||||||
import { getTopic } from '@renderer/hooks/useTopic'
|
|
||||||
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
|
import { fetchChatCompletion, fetchMessagesSummary } from '@renderer/services/api'
|
||||||
|
import { getDefaultTopic } from '@renderer/services/assistant'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
import { estimateHistoryTokenCount, filterMessages } from '@renderer/services/messages'
|
import { deleteMessageFiles, filterMessages, getContextCount } from '@renderer/services/messages'
|
||||||
import LocalStorage from '@renderer/services/storage'
|
import { estimateHistoryTokens, estimateMessageUsage } from '@renderer/services/tokens'
|
||||||
import { Assistant, Message, Model, Topic } from '@renderer/types'
|
import { Assistant, Message, Model, Topic } from '@renderer/types'
|
||||||
import { getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils'
|
import { getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import localforage from 'localforage'
|
import { flatten, last, reverse, take } from 'lodash'
|
||||||
import { last, reverse } from 'lodash'
|
|
||||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@@ -20,37 +20,51 @@ import Prompt from './Prompt'
|
|||||||
interface Props {
|
interface Props {
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
topic: Topic
|
topic: Topic
|
||||||
|
setActiveTopic: (topic: Topic) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const Messages: FC<Props> = ({ assistant, topic }) => {
|
const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||||
const [messages, setMessages] = useState<Message[]>([])
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
const [lastMessage, setLastMessage] = useState<Message | null>(null)
|
const [lastMessage, setLastMessage] = useState<Message | null>(null)
|
||||||
const provider = useProviderByAssistant(assistant)
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const { updateTopic } = useAssistant(assistant.id)
|
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
||||||
|
|
||||||
const onSendMessage = useCallback(
|
const onSendMessage = useCallback(
|
||||||
(message: Message) => {
|
async (message: Message) => {
|
||||||
|
if (message.role === 'user') {
|
||||||
|
estimateMessageUsage(message).then((usage) => {
|
||||||
|
setMessages((prev) => {
|
||||||
|
const _messages = prev.map((m) => (m.id === message.id ? { ...m, usage } : m))
|
||||||
|
db.topics.update(topic.id, { messages: _messages })
|
||||||
|
return _messages
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
const _messages = [...messages, message]
|
const _messages = [...messages, message]
|
||||||
setMessages(_messages)
|
setMessages(_messages)
|
||||||
localforage.setItem(`topic:${topic.id}`, { ...topic, messages: _messages })
|
db.topics.put({ id: topic.id, messages: _messages })
|
||||||
},
|
},
|
||||||
[messages, topic]
|
[messages, topic.id]
|
||||||
)
|
)
|
||||||
|
|
||||||
const autoRenameTopic = useCallback(async () => {
|
const autoRenameTopic = useCallback(async () => {
|
||||||
const _topic = getTopic(assistant, topic.id)
|
const _topic = getTopic(assistant, topic.id)
|
||||||
if (_topic && _topic.name === t('chat.default.topic.name') && messages.length >= 2) {
|
if (_topic && _topic.name === t('chat.default.topic.name') && messages.length >= 2) {
|
||||||
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
const summaryText = await fetchMessagesSummary({ messages, assistant })
|
||||||
summaryText && updateTopic({ ..._topic, name: summaryText })
|
if (summaryText) {
|
||||||
|
const data = { ..._topic, name: summaryText }
|
||||||
|
setActiveTopic(data)
|
||||||
|
updateTopic(data)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [assistant, messages, topic, updateTopic])
|
}, [assistant, messages, setActiveTopic, topic.id, updateTopic])
|
||||||
|
|
||||||
const onDeleteMessage = useCallback(
|
const onDeleteMessage = useCallback(
|
||||||
(message: Message) => {
|
(message: Message) => {
|
||||||
const _messages = messages.filter((m) => m.id !== message.id)
|
const _messages = messages.filter((m) => m.id !== message.id)
|
||||||
setMessages(_messages)
|
setMessages(_messages)
|
||||||
localforage.setItem(`topic:${topic.id}`, { id: topic.id, messages: _messages })
|
db.topics.update(topic.id, { messages: _messages })
|
||||||
|
deleteMessageFiles(message)
|
||||||
},
|
},
|
||||||
[messages, topic.id]
|
[messages, topic.id]
|
||||||
)
|
)
|
||||||
@@ -58,10 +72,15 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
|
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
|
||||||
onSendMessage(msg)
|
await onSendMessage(msg)
|
||||||
fetchChatCompletion({ assistant, messages: [...messages, msg], topic, onResponse: setLastMessage })
|
fetchChatCompletion({
|
||||||
|
assistant,
|
||||||
|
messages: [...messages, msg],
|
||||||
|
topic,
|
||||||
|
onResponse: setLastMessage
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => {
|
EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, async (msg: Message) => {
|
||||||
setLastMessage(null)
|
setLastMessage(null)
|
||||||
onSendMessage(msg)
|
onSendMessage(msg)
|
||||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
|
||||||
@@ -83,15 +102,67 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
|||||||
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
|
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
|
||||||
setMessages([])
|
setMessages([])
|
||||||
updateTopic({ ...topic, messages: [] })
|
updateTopic({ ...topic, messages: [] })
|
||||||
LocalStorage.clearTopicMessages(topic.id)
|
TopicManager.clearTopicMessages(topic.id)
|
||||||
|
}),
|
||||||
|
EventEmitter.on(EVENT_NAMES.NEW_CONTEXT, () => {
|
||||||
|
const lastMessage = last(messages)
|
||||||
|
|
||||||
|
if (lastMessage && lastMessage.type === 'clear') {
|
||||||
|
onDeleteMessage(lastMessage)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onSendMessage({
|
||||||
|
id: uuid(),
|
||||||
|
assistantId: assistant.id,
|
||||||
|
role: 'user',
|
||||||
|
content: '',
|
||||||
|
topicId: topic.id,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
status: 'success',
|
||||||
|
type: 'clear'
|
||||||
|
} as Message)
|
||||||
|
}),
|
||||||
|
EventEmitter.on(EVENT_NAMES.NEW_BRANCH, async (index: number) => {
|
||||||
|
const newTopic = getDefaultTopic()
|
||||||
|
newTopic.name = topic.name
|
||||||
|
const branchMessages = take(messages, messages.length - index)
|
||||||
|
|
||||||
|
// 将分支的消息放入数据库
|
||||||
|
await db.topics.add({ id: newTopic.id, messages: branchMessages })
|
||||||
|
addTopic(newTopic)
|
||||||
|
setActiveTopic(newTopic)
|
||||||
|
autoRenameTopic()
|
||||||
|
|
||||||
|
// 由于复制了消息,消息中附带的文件的总数变了,需要更新
|
||||||
|
const filesArr = branchMessages.map((m) => m.files)
|
||||||
|
const files = flatten(filesArr).filter(Boolean)
|
||||||
|
files.map(async (f) => {
|
||||||
|
const file = await db.files.get({ id: f?.id })
|
||||||
|
file && db.files.update(file.id, { count: file.count + 1 })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
return () => unsubscribes.forEach((unsub) => unsub())
|
return () => unsubscribes.forEach((unsub) => unsub())
|
||||||
}, [assistant, messages, provider, topic, autoRenameTopic, updateTopic, onSendMessage])
|
}, [
|
||||||
|
addTopic,
|
||||||
|
assistant,
|
||||||
|
autoRenameTopic,
|
||||||
|
messages,
|
||||||
|
onDeleteMessage,
|
||||||
|
onSendMessage,
|
||||||
|
setActiveTopic,
|
||||||
|
topic,
|
||||||
|
updateTopic
|
||||||
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
runAsyncFunction(async () => {
|
runAsyncFunction(async () => {
|
||||||
const messages = (await LocalStorage.getTopicMessages(topic.id)) || []
|
const messages = (await TopicManager.getTopicMessages(topic.id)) || []
|
||||||
setMessages(messages)
|
setMessages(messages)
|
||||||
})
|
})
|
||||||
}, [topic.id])
|
}, [topic.id])
|
||||||
@@ -101,13 +172,18 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
|||||||
}, [messages])
|
}, [messages])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, estimateHistoryTokenCount(assistant, messages))
|
runAsyncFunction(async () => {
|
||||||
|
EventEmitter.emit(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, {
|
||||||
|
tokensCount: await estimateHistoryTokens(assistant, messages),
|
||||||
|
contextCount: getContextCount(assistant, messages)
|
||||||
|
})
|
||||||
|
})
|
||||||
}, [assistant, messages])
|
}, [assistant, messages])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container id="messages" key={assistant.id} ref={containerRef}>
|
<Container id="messages" key={assistant.id} ref={containerRef}>
|
||||||
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
|
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
|
||||||
{lastMessage && <MessageItem message={lastMessage} />}
|
{lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} />}
|
||||||
{reverse([...messages]).map((message, index) => (
|
{reverse([...messages]).map((message, index) => (
|
||||||
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
|
<MessageItem key={message.id} message={message} showMenu index={index} onDeleteMessage={onDeleteMessage} />
|
||||||
))}
|
))}
|
||||||
@@ -117,6 +193,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|||||||
132
src/renderer/src/pages/home/Navbar.tsx
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||||
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import AddAssistantPopup from '@renderer/components/Popups/AddAssistantPopup'
|
||||||
|
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
|
||||||
|
import { isMac, isWindows } from '@renderer/config/constant'
|
||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||||
|
import { syncAsistantToAgent } from '@renderer/services/assistant'
|
||||||
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
|
import { Switch } from 'antd'
|
||||||
|
import { FC, useCallback } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import SelectModelButton from './components/SelectModelButton'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
activeAssistant: Assistant
|
||||||
|
activeTopic: Topic
|
||||||
|
setActiveAssistant: (assistant: Assistant) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const HeaderNavbar: FC<Props> = ({ activeAssistant, setActiveAssistant }) => {
|
||||||
|
const { assistant, updateAssistant } = useAssistant(activeAssistant.id)
|
||||||
|
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||||
|
const { theme, toggleTheme } = useTheme()
|
||||||
|
const { topicPosition } = useSettings()
|
||||||
|
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||||
|
|
||||||
|
const onCreateAssistant = async () => {
|
||||||
|
const assistant = await AddAssistantPopup.show()
|
||||||
|
assistant && setActiveAssistant(assistant)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onEditAssistant = useCallback(async () => {
|
||||||
|
const _assistant = await AssistantSettingPopup.show({ assistant })
|
||||||
|
updateAssistant(_assistant)
|
||||||
|
syncAsistantToAgent(_assistant)
|
||||||
|
}, [assistant, updateAssistant])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Navbar>
|
||||||
|
{showAssistants && (
|
||||||
|
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
|
||||||
|
<NewButton onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
|
||||||
|
<i className="iconfont icon-hide-sidebar" />
|
||||||
|
</NewButton>
|
||||||
|
<NewButton onClick={onCreateAssistant}>
|
||||||
|
<i className="iconfont icon-a-addchat" />
|
||||||
|
</NewButton>
|
||||||
|
</NavbarLeft>
|
||||||
|
)}
|
||||||
|
<NavbarRight style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}>
|
||||||
|
<HStack alignItems="center">
|
||||||
|
{!showAssistants && (
|
||||||
|
<NewButton
|
||||||
|
onClick={() => toggleShowAssistants()}
|
||||||
|
style={{ marginRight: isMac ? 8 : 25, marginLeft: isMac ? 4 : 0 }}>
|
||||||
|
<i className="iconfont icon-show-sidebar" />
|
||||||
|
</NewButton>
|
||||||
|
)}
|
||||||
|
<TitleText style={{ marginRight: 10, cursor: 'pointer' }} className="nodrag" onClick={onEditAssistant}>
|
||||||
|
{assistant.name}
|
||||||
|
</TitleText>
|
||||||
|
<SelectModelButton assistant={assistant} />
|
||||||
|
</HStack>
|
||||||
|
<HStack alignItems="center">
|
||||||
|
<ThemeSwitch
|
||||||
|
checkedChildren={<i className="iconfont icon-theme icon-dark1" />}
|
||||||
|
unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />}
|
||||||
|
checked={theme === 'dark'}
|
||||||
|
onChange={toggleTheme}
|
||||||
|
/>
|
||||||
|
{topicPosition === 'right' && (
|
||||||
|
<NewButton onClick={toggleShowTopics}>
|
||||||
|
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
|
||||||
|
</NewButton>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</NavbarRight>
|
||||||
|
</Navbar>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NewButton = styled.div`
|
||||||
|
-webkit-app-region: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 7px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
.iconfont {
|
||||||
|
font-size: 19px;
|
||||||
|
color: var(--color-icon);
|
||||||
|
&.icon-a-addchat {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
&.icon-a-darkmode {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.anticon {
|
||||||
|
color: var(--color-icon);
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
color: var(--color-icon-white);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TitleText = styled.span`
|
||||||
|
margin-left: 5px;
|
||||||
|
font-family: Ubuntu;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
`
|
||||||
|
|
||||||
|
const ThemeSwitch = styled(Switch)`
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
margin-right: 10px;
|
||||||
|
.icon-theme {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default HeaderNavbar
|
||||||
142
src/renderer/src/pages/home/RightSidebar.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { BarsOutlined, SettingOutlined } from '@ant-design/icons'
|
||||||
|
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||||
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
|
import { uuid } from '@renderer/utils'
|
||||||
|
import { Segmented, SegmentedProps } from 'antd'
|
||||||
|
import { FC, useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import Assistants from './Assistants'
|
||||||
|
import Settings from './Settings'
|
||||||
|
import Topics from './Topics'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
activeAssistant: Assistant
|
||||||
|
activeTopic: Topic
|
||||||
|
setActiveAssistant: (assistant: Assistant) => void
|
||||||
|
setActiveTopic: (topic: Topic) => void
|
||||||
|
position: 'left' | 'right'
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tab = 'assistants' | 'topic' | 'settings'
|
||||||
|
|
||||||
|
let _tab: any = ''
|
||||||
|
|
||||||
|
const RightSidebar: FC<Props> = ({ activeAssistant, activeTopic, setActiveAssistant, setActiveTopic, position }) => {
|
||||||
|
const { addAssistant } = useAssistants()
|
||||||
|
const [tab, setTab] = useState<Tab>(position === 'left' ? _tab || 'assistants' : 'topic')
|
||||||
|
const { topicPosition } = useSettings()
|
||||||
|
const { defaultAssistant } = useDefaultAssistant()
|
||||||
|
const { toggleShowTopics } = useShowTopics()
|
||||||
|
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const borderStyle = '0.5px solid var(--color-border)'
|
||||||
|
const border = position === 'left' ? { borderRight: borderStyle } : { borderLeft: borderStyle }
|
||||||
|
|
||||||
|
if (position === 'left' && topicPosition === 'left') {
|
||||||
|
_tab = tab
|
||||||
|
}
|
||||||
|
|
||||||
|
const showTab = !(position === 'left' && topicPosition === 'right')
|
||||||
|
const assistantTab = {
|
||||||
|
label: t('common.assistant'),
|
||||||
|
value: 'assistants',
|
||||||
|
icon: <i className="iconfont icon-business-smart-assistant" />
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCreateDefaultAssistant = () => {
|
||||||
|
const assistant = { ...defaultAssistant, id: uuid() }
|
||||||
|
addAssistant(assistant)
|
||||||
|
setActiveAssistant(assistant)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribes = [
|
||||||
|
EventEmitter.on(EVENT_NAMES.SHOW_ASSISTANTS, (): any => {
|
||||||
|
showTab && setTab('assistants')
|
||||||
|
}),
|
||||||
|
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => {
|
||||||
|
showTab && setTab('topic')
|
||||||
|
}),
|
||||||
|
EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => {
|
||||||
|
showTab && setTab('settings')
|
||||||
|
}),
|
||||||
|
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => {
|
||||||
|
showTab && setTab('topic')
|
||||||
|
if (position === 'left' && topicPosition === 'right') {
|
||||||
|
toggleShowTopics()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
return () => unsubscribes.forEach((unsub) => unsub())
|
||||||
|
}, [position, showTab, tab, toggleShowTopics, topicPosition])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (position === 'right' && topicPosition === 'right' && tab === 'assistants') {
|
||||||
|
setTab('topic')
|
||||||
|
}
|
||||||
|
if (position === 'left' && topicPosition === 'right' && tab !== 'assistants') {
|
||||||
|
setTab('assistants')
|
||||||
|
}
|
||||||
|
}, [position, tab, topicPosition])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container style={border}>
|
||||||
|
{showTab && (
|
||||||
|
<Segmented
|
||||||
|
value={tab}
|
||||||
|
className="segmented-tab"
|
||||||
|
style={{ borderRadius: 0, padding: '10px', gap: 2, borderBottom: borderStyle }}
|
||||||
|
options={
|
||||||
|
[
|
||||||
|
position === 'left' && topicPosition === 'left' ? assistantTab : undefined,
|
||||||
|
{ label: t('common.topics'), value: 'topic', icon: <BarsOutlined /> },
|
||||||
|
{ label: t('settings.title'), value: 'settings', icon: <SettingOutlined /> }
|
||||||
|
].filter(Boolean) as SegmentedProps['options']
|
||||||
|
}
|
||||||
|
onChange={(value) => setTab(value as 'topic' | 'settings')}
|
||||||
|
block
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<TabContent>
|
||||||
|
{tab === 'assistants' && (
|
||||||
|
<Assistants
|
||||||
|
activeAssistant={activeAssistant}
|
||||||
|
setActiveAssistant={setActiveAssistant}
|
||||||
|
onCreateAssistant={onCreateDefaultAssistant}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{tab === 'topic' && (
|
||||||
|
<Topics assistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
|
||||||
|
)}
|
||||||
|
{tab === 'settings' && <Settings assistant={activeAssistant} />}
|
||||||
|
</TabContent>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: var(--assistants-width);
|
||||||
|
height: calc(100vh - var(--navbar-height));
|
||||||
|
.collapsed {
|
||||||
|
width: 0;
|
||||||
|
border-left: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TabContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default RightSidebar
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
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: 4px;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
font-family: Ubuntu;
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
background-color: var(--color-background-mute);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default TopicsTab
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
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
|
|
||||||
activeTopic: Topic
|
|
||||||
setActiveTopic: (topic: Topic) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const RightSidebar: FC<Props> = (props) => {
|
|
||||||
const [tab, setTab] = useState<'topic' | 'settings'>('topic')
|
|
||||||
const { rightSidebarShown, showRightSidebar, hideRightSidebar } = useShowRightSidebar()
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const isTopicTab = tab === 'topic'
|
|
||||||
const isSettingsTab = tab === 'settings'
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribes = [
|
|
||||||
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (): any => {
|
|
||||||
if (rightSidebarShown && isTopicTab) {
|
|
||||||
return hideRightSidebar()
|
|
||||||
}
|
|
||||||
if (rightSidebarShown) {
|
|
||||||
return setTab('topic')
|
|
||||||
}
|
|
||||||
showRightSidebar()
|
|
||||||
setTab('topic')
|
|
||||||
}),
|
|
||||||
EventEmitter.on(EVENT_NAMES.SHOW_CHAT_SETTINGS, (): any => {
|
|
||||||
if (rightSidebarShown && isSettingsTab) {
|
|
||||||
return hideRightSidebar()
|
|
||||||
}
|
|
||||||
if (rightSidebarShown) {
|
|
||||||
return setTab('settings')
|
|
||||||
}
|
|
||||||
showRightSidebar()
|
|
||||||
setTab('settings')
|
|
||||||
}),
|
|
||||||
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => setTab('topic'))
|
|
||||||
]
|
|
||||||
return () => unsubscribes.forEach((unsub) => unsub())
|
|
||||||
}, [hideRightSidebar, isSettingsTab, isTopicTab, rightSidebarShown, showRightSidebar])
|
|
||||||
|
|
||||||
if (!rightSidebarShown) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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);
|
|
||||||
.collapsed {
|
|
||||||
width: 0;
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const TabContent = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: column;
|
|
||||||
overflow-y: auto;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default RightSidebar
|
|
||||||
@@ -8,6 +8,7 @@ import { useAppDispatch } from '@renderer/store'
|
|||||||
import {
|
import {
|
||||||
setFontSize,
|
setFontSize,
|
||||||
setMessageFont,
|
setMessageFont,
|
||||||
|
setPasteLongTextAsFile,
|
||||||
setShowInputEstimatedTokens,
|
setShowInputEstimatedTokens,
|
||||||
setShowMessageDivider
|
setShowMessageDivider
|
||||||
} from '@renderer/store/settings'
|
} from '@renderer/store/settings'
|
||||||
@@ -33,8 +34,14 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const { showMessageDivider, messageFont, showInputEstimatedTokens, sendMessageShortcut, setSendMessageShortcut } =
|
const {
|
||||||
useSettings()
|
showMessageDivider,
|
||||||
|
messageFont,
|
||||||
|
showInputEstimatedTokens,
|
||||||
|
sendMessageShortcut,
|
||||||
|
setSendMessageShortcut,
|
||||||
|
pasteLongTextAsFile
|
||||||
|
} = useSettings()
|
||||||
|
|
||||||
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||||
updateAssistantSettings({
|
updateAssistantSettings({
|
||||||
@@ -87,7 +94,7 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<SettingSubtitle>
|
<SettingSubtitle style={{ marginTop: 5 }}>
|
||||||
{t('settings.messages.model.title')}{' '}
|
{t('settings.messages.model.title')}{' '}
|
||||||
<Tooltip title={t('chat.settings.reset')}>
|
<Tooltip title={t('chat.settings.reset')}>
|
||||||
<ReloadOutlined onClick={onReset} style={{ cursor: 'pointer', fontSize: 12, padding: '0 3px' }} />
|
<ReloadOutlined onClick={onReset} style={{ cursor: 'pointer', fontSize: 12, padding: '0 3px' }} />
|
||||||
@@ -104,7 +111,7 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<Slider
|
<Slider
|
||||||
min={0}
|
min={0}
|
||||||
max={1.2}
|
max={2}
|
||||||
onChange={setTemperature}
|
onChange={setTemperature}
|
||||||
onChangeComplete={onTemperatureChange}
|
onChangeComplete={onTemperatureChange}
|
||||||
value={typeof temperature === 'number' ? temperature : 0}
|
value={typeof temperature === 'number' ? temperature : 0}
|
||||||
@@ -187,10 +194,7 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
<Slider
|
<Slider
|
||||||
value={fontSizeValue}
|
value={fontSizeValue}
|
||||||
onChange={(value) => setFontSizeValue(value)}
|
onChange={(value) => setFontSizeValue(value)}
|
||||||
onChangeComplete={(value) => {
|
onChangeComplete={(value) => dispatch(setFontSize(value))}
|
||||||
dispatch(setFontSize(value))
|
|
||||||
console.debug('set font size', value)
|
|
||||||
}}
|
|
||||||
min={12}
|
min={12}
|
||||||
max={18}
|
max={18}
|
||||||
step={1}
|
step={1}
|
||||||
@@ -213,6 +217,15 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('settings.messages.input.paste_long_text_as_file')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={pasteLongTextAsFile}
|
||||||
|
onChange={(checked) => dispatch(setPasteLongTextAsFile(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
|
<SettingRowTitleSmall>{t('settings.messages.input.send_shortcuts')}</SettingRowTitleSmall>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
@@ -234,7 +247,9 @@ const Container = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0 15px;
|
overflow: hidden;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
padding: 10px 15px;
|
||||||
`
|
`
|
||||||
|
|
||||||
const Label = styled.p`
|
const Label = styled.p`
|
||||||
224
src/renderer/src/pages/home/Topics.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import { CloseOutlined, DeleteOutlined, EditOutlined, FolderOutlined } from '@ant-design/icons'
|
||||||
|
import DragableList from '@renderer/components/DragableList'
|
||||||
|
import PromptPopup from '@renderer/components/Popups/PromptPopup'
|
||||||
|
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
|
import { TopicManager } from '@renderer/hooks/useTopic'
|
||||||
|
import { fetchMessagesSummary } from '@renderer/services/api'
|
||||||
|
import { useAppSelector } from '@renderer/store'
|
||||||
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
|
import { Dropdown, MenuProps } from 'antd'
|
||||||
|
import { findIndex } from 'lodash'
|
||||||
|
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 Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic }) => {
|
||||||
|
const { assistants } = useAssistants()
|
||||||
|
const { assistant, removeTopic, moveTopic, updateTopic, updateTopics } = useAssistant(_assistant.id)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const generating = useAppSelector((state) => state.runtime.generating)
|
||||||
|
|
||||||
|
const onDeleteTopic = useCallback(
|
||||||
|
(topic: Topic) => {
|
||||||
|
if (generating) {
|
||||||
|
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (assistant.topics.length > 1) {
|
||||||
|
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
|
||||||
|
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
|
||||||
|
removeTopic(topic)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[assistant.topics, generating, removeTopic, setActiveTopic, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onMoveTopic = useCallback(
|
||||||
|
(topic: Topic, toAssistant: Assistant) => {
|
||||||
|
if (generating) {
|
||||||
|
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const index = findIndex(assistant.topics, (t) => t.id === topic.id)
|
||||||
|
setActiveTopic(assistant.topics[index + 1 === assistant.topics.length ? 0 : index + 1])
|
||||||
|
moveTopic(topic, toAssistant)
|
||||||
|
},
|
||||||
|
[assistant.topics, generating, moveTopic, setActiveTopic, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onSwitchTopic = useCallback(
|
||||||
|
(topic: Topic) => {
|
||||||
|
if (generating) {
|
||||||
|
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setActiveTopic(topic)
|
||||||
|
},
|
||||||
|
[generating, setActiveTopic, t]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getTopicMenuItems = useCallback(
|
||||||
|
(topic: Topic) => {
|
||||||
|
const menus: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
label: t('chat.topics.auto_rename'),
|
||||||
|
key: 'auto-rename',
|
||||||
|
icon: <i className="iconfont icon-business-smart-assistant" style={{ fontSize: '14px' }} />,
|
||||||
|
async onClick() {
|
||||||
|
const messages = await TopicManager.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 (assistants.length > 1 && assistant.topics.length > 1) {
|
||||||
|
menus.push({
|
||||||
|
label: t('chat.topics.move_to'),
|
||||||
|
key: 'move',
|
||||||
|
icon: <FolderOutlined />,
|
||||||
|
children: assistants
|
||||||
|
.filter((a) => a.id !== assistant.id)
|
||||||
|
.map((a) => ({
|
||||||
|
label: a.name,
|
||||||
|
key: a.id,
|
||||||
|
onClick: () => onMoveTopic(topic, a)
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assistant.topics.length > 1) {
|
||||||
|
menus.push({ type: 'divider' })
|
||||||
|
menus.push({
|
||||||
|
label: t('common.delete'),
|
||||||
|
danger: true,
|
||||||
|
key: 'delete',
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
onClick: () => onDeleteTopic(topic)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return menus
|
||||||
|
},
|
||||||
|
[assistant, assistants, onDeleteTopic, onMoveTopic, t, updateTopic]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<DragableList list={assistant.topics} onUpdate={updateTopics}>
|
||||||
|
{(topic) => {
|
||||||
|
const isActive = topic.id === activeTopic?.id
|
||||||
|
return (
|
||||||
|
<Dropdown menu={{ items: getTopicMenuItems(topic) }} trigger={['contextMenu']} key={topic.id}>
|
||||||
|
<TopicListItem className={isActive ? 'active' : ''} onClick={() => onSwitchTopic(topic)}>
|
||||||
|
<TopicName>{topic.name}</TopicName>
|
||||||
|
{assistant.topics.length > 1 && (
|
||||||
|
<MenuButton
|
||||||
|
className="menu"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onDeleteTopic(topic)
|
||||||
|
}}>
|
||||||
|
<CloseOutlined />
|
||||||
|
</MenuButton>
|
||||||
|
)}
|
||||||
|
</TopicListItem>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</DragableList>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 10px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
max-height: calc(100vh - var(--navbar-height) - 140px);
|
||||||
|
`
|
||||||
|
|
||||||
|
const TopicListItem = styled.div`
|
||||||
|
padding: 7px 10px;
|
||||||
|
margin: 0 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: Ubuntu;
|
||||||
|
font-size: 13px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
.menu {
|
||||||
|
opacity: 0;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
font-weight: 500;
|
||||||
|
.menu {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TopicName = styled.div`
|
||||||
|
color: var(--color-text);
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 13px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const MenuButton = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
min-height: 24px;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 5px;
|
||||||
|
.anticon {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default Topics
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||||
|
import VisionIcon from '@renderer/components/Icons/VisionIcon'
|
||||||
import { isLocalAi } from '@renderer/config/env'
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
|
import { isVisionModel } from '@renderer/config/models'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { Assistant } from '@renderer/types'
|
import { Assistant } from '@renderer/types'
|
||||||
import { Button } from 'antd'
|
import { Button } from 'antd'
|
||||||
@@ -23,10 +25,11 @@ const SelectModelButton: FC<Props> = ({ assistant }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SelectModelDropdown model={model} onSelect={setModel}>
|
<SelectModelDropdown model={model} onSelect={setModel} placement="top">
|
||||||
<DropdownButton size="small" type="default">
|
<DropdownButton size="small" type="default">
|
||||||
<ModelAvatar model={model} size={20} />
|
<ModelAvatar model={model} size={20} />
|
||||||
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
|
<ModelName>{model ? upperFirst(model.name) : t('button.select_model')}</ModelName>
|
||||||
|
{isVisionModel(model) && <VisionIcon style={{ marginLeft: 0 }} />}
|
||||||
</DropdownButton>
|
</DropdownButton>
|
||||||
</SelectModelDropdown>
|
</SelectModelDropdown>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
|
import VisionIcon from '@renderer/components/Icons/VisionIcon'
|
||||||
|
import { isVisionModel } from '@renderer/config/models'
|
||||||
import { getModelLogo } from '@renderer/config/provider'
|
import { getModelLogo } from '@renderer/config/provider'
|
||||||
import { useProviders } from '@renderer/hooks/useProvider'
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
|
import { getModelUniqId } from '@renderer/services/model'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd'
|
import { Avatar, Dropdown, DropdownProps, MenuProps } from 'antd'
|
||||||
import { first, reverse, sortBy, upperFirst } from 'lodash'
|
import { first, reverse, sortBy, upperFirst } from 'lodash'
|
||||||
@@ -8,7 +11,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
interface Props extends DropdownProps {
|
interface Props extends DropdownProps {
|
||||||
model: Model
|
model?: Model
|
||||||
onSelect: (model: Model) => void
|
onSelect: (model: Model) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,9 +26,12 @@ const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, o
|
|||||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||||
type: 'group',
|
type: 'group',
|
||||||
children: reverse(sortBy(p.models, 'name')).map((m) => ({
|
children: reverse(sortBy(p.models, 'name')).map((m) => ({
|
||||||
key: m?.id,
|
key: getModelUniqId(m),
|
||||||
label: upperFirst(m?.name),
|
label: (
|
||||||
defaultSelectedKeys: [model?.id],
|
<div>
|
||||||
|
{upperFirst(m?.name)} {isVisionModel(m) && <VisionIcon />}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
icon: (
|
icon: (
|
||||||
<Avatar src={getModelLogo(m?.id || '')} size={24}>
|
<Avatar src={getModelLogo(m?.id || '')} size={24}>
|
||||||
{first(m?.name)}
|
{first(m?.name)}
|
||||||
@@ -37,7 +43,11 @@ const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, o
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
menu={{ items, style: { maxHeight: '80vh', overflow: 'auto' }, selectedKeys: [model?.id] }}
|
menu={{
|
||||||
|
items,
|
||||||
|
style: { maxHeight: '55vh', overflow: 'auto' },
|
||||||
|
selectedKeys: model ? [getModelUniqId(model)] : []
|
||||||
|
}}
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
arrow
|
arrow
|
||||||
placement="bottom"
|
placement="bottom"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const Suggestions: FC<Props> = ({ assistant, messages, lastMessage }) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
EventEmitter.on(EVENT_NAMES.AI_CHAT_COMPLETION, async (msg: Message) => {
|
EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, async (msg: Message) => {
|
||||||
setLoadingSuggestions(true)
|
setLoadingSuggestions(true)
|
||||||
const _suggestions = await fetchSuggestions({ assistant, messages: [...messages, msg] })
|
const _suggestions = await fetchSuggestions({ assistant, messages: [...messages, msg] })
|
||||||
if (_suggestions.length) {
|
if (_suggestions.length) {
|
||||||
|
|||||||
@@ -108,18 +108,18 @@ const AssistantSettings: FC = () => {
|
|||||||
<Col span={21}>
|
<Col span={21}>
|
||||||
<Slider
|
<Slider
|
||||||
min={0}
|
min={0}
|
||||||
max={1.2}
|
max={2}
|
||||||
onChange={setTemperature}
|
onChange={setTemperature}
|
||||||
onChangeComplete={onTemperatureChange}
|
onChangeComplete={onTemperatureChange}
|
||||||
value={typeof temperature === 'number' ? temperature : 0}
|
value={typeof temperature === 'number' ? temperature : 0}
|
||||||
marks={{ 0: '0', 0.7: '0.7', 1: '1', 1.2: '1.2' }}
|
marks={{ 0: '0', 0.7: '0.7', 2: '2' }}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={3}>
|
<Col span={3}>
|
||||||
<InputNumber
|
<InputNumber
|
||||||
min={0}
|
min={0}
|
||||||
max={1.2}
|
max={2}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
value={temperature}
|
value={temperature}
|
||||||
onChange={onTemperatureChange}
|
onChange={onTemperatureChange}
|
||||||
|
|||||||
@@ -1,25 +1,32 @@
|
|||||||
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
import { isMac } from '@renderer/config/constant'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import { backup, reset, restore } from '@renderer/services/backup'
|
import { backup, reset, restore } from '@renderer/services/backup'
|
||||||
import LocalStorage from '@renderer/services/storage'
|
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { setAvatar } from '@renderer/store/runtime'
|
import { setClickAssistantToShowTopic, setLanguage } from '@renderer/store/settings'
|
||||||
import { setLanguage, setUserName, ThemeMode } from '@renderer/store/settings'
|
|
||||||
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
import { setProxyUrl as _setProxyUrl } from '@renderer/store/settings'
|
||||||
import { compressImage, isValidProxyUrl } from '@renderer/utils'
|
import { ThemeMode } from '@renderer/types'
|
||||||
import { Avatar, Button, Input, Select, Upload } from 'antd'
|
import { isValidProxyUrl } from '@renderer/utils'
|
||||||
|
import { Button, Input, Select, Switch } from 'antd'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '.'
|
import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '.'
|
||||||
|
|
||||||
const GeneralSettings: FC = () => {
|
const GeneralSettings: FC = () => {
|
||||||
const avatar = useAvatar()
|
const {
|
||||||
const { language, proxyUrl: storeProxyUrl, userName, theme, setTheme } = useSettings()
|
language,
|
||||||
|
proxyUrl: storeProxyUrl,
|
||||||
|
theme,
|
||||||
|
windowStyle,
|
||||||
|
topicPosition,
|
||||||
|
clickAssistantToShowTopic,
|
||||||
|
setTheme,
|
||||||
|
setWindowStyle,
|
||||||
|
setTopicPosition
|
||||||
|
} = useSettings()
|
||||||
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
const [proxyUrl, setProxyUrl] = useState<string | undefined>(storeProxyUrl)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -48,7 +55,7 @@ const GeneralSettings: FC = () => {
|
|||||||
<SettingRowTitle>{t('common.language')}</SettingRowTitle>
|
<SettingRowTitle>{t('common.language')}</SettingRowTitle>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={language || 'en-US'}
|
defaultValue={language || 'en-US'}
|
||||||
style={{ width: 120 }}
|
style={{ width: 180 }}
|
||||||
onChange={onSelectLanguage}
|
onChange={onSelectLanguage}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'zh-CN', label: '中文' },
|
{ value: 'zh-CN', label: '中文' },
|
||||||
@@ -61,7 +68,7 @@ const GeneralSettings: FC = () => {
|
|||||||
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.theme.title')}</SettingRowTitle>
|
||||||
<Select
|
<Select
|
||||||
defaultValue={theme}
|
defaultValue={theme}
|
||||||
style={{ width: 120 }}
|
style={{ width: 180 }}
|
||||||
onChange={setTheme}
|
onChange={setTheme}
|
||||||
options={[
|
options={[
|
||||||
{ value: ThemeMode.light, label: t('settings.theme.light') },
|
{ value: ThemeMode.light, label: t('settings.theme.light') },
|
||||||
@@ -70,46 +77,56 @@ const GeneralSettings: FC = () => {
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
|
{isMac && (
|
||||||
|
<>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitle>{t('settings.theme.window.style.title')}</SettingRowTitle>
|
||||||
|
<Select
|
||||||
|
defaultValue={windowStyle || 'opaque'}
|
||||||
|
style={{ width: 180 }}
|
||||||
|
onChange={setWindowStyle}
|
||||||
|
options={[
|
||||||
|
{ value: 'transparent', label: t('settings.theme.window.style.transparent') },
|
||||||
|
{ value: 'opaque', label: t('settings.theme.window.style.opaque') }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('common.avatar')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.topic.position')}</SettingRowTitle>
|
||||||
<Upload
|
<Select
|
||||||
customRequest={() => {}}
|
defaultValue={topicPosition || 'right'}
|
||||||
accept="image/png, image/jpeg"
|
style={{ width: 180 }}
|
||||||
itemRender={() => null}
|
onChange={setTopicPosition}
|
||||||
maxCount={1}
|
options={[
|
||||||
onChange={async ({ file }) => {
|
{ value: 'left', label: t('settings.topic.position.left') },
|
||||||
try {
|
{ value: 'right', label: t('settings.topic.position.right') }
|
||||||
const _file = file.originFileObj as File
|
]}
|
||||||
const compressedFile = await compressImage(_file)
|
|
||||||
await LocalStorage.storeImage('avatar', compressedFile)
|
|
||||||
dispatch(setAvatar(await LocalStorage.getImage('avatar')))
|
|
||||||
} catch (error: any) {
|
|
||||||
window.message.error(error.message)
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<UserAvatar src={avatar} size="large" />
|
|
||||||
</Upload>
|
|
||||||
</SettingRow>
|
|
||||||
<SettingDivider />
|
|
||||||
<SettingRow>
|
|
||||||
<SettingRowTitle>{t('settings.general.user_name')}</SettingRowTitle>
|
|
||||||
<Input
|
|
||||||
placeholder={t('settings.general.user_name.placeholder')}
|
|
||||||
value={userName}
|
|
||||||
onChange={(e) => dispatch(setUserName(e.target.value))}
|
|
||||||
style={{ width: 150 }}
|
|
||||||
maxLength={30}
|
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
|
{topicPosition === 'left' && (
|
||||||
|
<>
|
||||||
|
<SettingRow style={{ minHeight: 32 }}>
|
||||||
|
<SettingRowTitle>{t('settings.advanced.click_assistant_switch_to_topics')}</SettingRowTitle>
|
||||||
|
<Switch
|
||||||
|
checked={clickAssistantToShowTopic}
|
||||||
|
onChange={(checked) => dispatch(setClickAssistantToShowTopic(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
|
||||||
<Input
|
<Input
|
||||||
placeholder="socks5://127.0.0.1:6153"
|
placeholder="socks5://127.0.0.1:6153"
|
||||||
value={proxyUrl}
|
value={proxyUrl}
|
||||||
onChange={(e) => setProxyUrl(e.target.value)}
|
onChange={(e) => setProxyUrl(e.target.value)}
|
||||||
style={{ width: 300 }}
|
style={{ width: 180 }}
|
||||||
onBlur={() => onSetProxyUrl()}
|
onBlur={() => onSetProxyUrl()}
|
||||||
type="url"
|
type="url"
|
||||||
/>
|
/>
|
||||||
@@ -117,12 +134,12 @@ const GeneralSettings: FC = () => {
|
|||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||||
<HStack gap="5px">
|
<HStack gap="5px" justifyContent="space-between">
|
||||||
<Button onClick={backup} icon={<SaveOutlined />}>
|
<Button onClick={backup} icon={<SaveOutlined />}>
|
||||||
备份
|
{t('settings.general.backup.button')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={restore} icon={<FolderOpenOutlined />}>
|
<Button onClick={restore} icon={<FolderOpenOutlined />}>
|
||||||
恢复
|
{t('settings.general.restore.button')}
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
@@ -140,8 +157,4 @@ const GeneralSettings: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserAvatar = styled(Avatar)`
|
|
||||||
cursor: pointer;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default GeneralSettings
|
export default GeneralSettings
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { EditOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
|
import { EditOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||||
import { useProviders } from '@renderer/hooks/useProvider'
|
import { useProviders } from '@renderer/hooks/useProvider'
|
||||||
|
import { getModelUniqId, hasModel } from '@renderer/services/model'
|
||||||
import { Model } from '@renderer/types'
|
import { Model } from '@renderer/types'
|
||||||
import { Select } from 'antd'
|
import { Select } from 'antd'
|
||||||
import { find, sortBy, upperFirst } from 'lodash'
|
import { find, sortBy, upperFirst } from 'lodash'
|
||||||
import { FC } from 'react'
|
import { FC, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { SettingContainer, SettingDivider, SettingTitle } from '.'
|
import { SettingContainer, SettingDivider, SettingTitle } from '.'
|
||||||
@@ -23,11 +24,24 @@ const ModelSettings: FC = () => {
|
|||||||
title: p.name,
|
title: p.name,
|
||||||
options: sortBy(p.models, 'name').map((m) => ({
|
options: sortBy(p.models, 'name').map((m) => ({
|
||||||
label: upperFirst(m.name),
|
label: upperFirst(m.name),
|
||||||
value: m.id
|
value: getModelUniqId(m)
|
||||||
}))
|
}))
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const iconStyle = { fontSize: 16, marginRight: 8 }
|
const defaultModelValue = useMemo(
|
||||||
|
() => (hasModel(defaultModel) ? getModelUniqId(defaultModel) : undefined),
|
||||||
|
[defaultModel]
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultTopicNamingModel = useMemo(
|
||||||
|
() => (hasModel(topicNamingModel) ? getModelUniqId(topicNamingModel) : undefined),
|
||||||
|
[topicNamingModel]
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultTranslateModel = useMemo(
|
||||||
|
() => (hasModel(translateModel) ? getModelUniqId(translateModel) : undefined),
|
||||||
|
[translateModel]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingContainer>
|
<SettingContainer>
|
||||||
@@ -39,10 +53,12 @@ const ModelSettings: FC = () => {
|
|||||||
</SettingTitle>
|
</SettingTitle>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<Select
|
<Select
|
||||||
defaultValue={defaultModel.id}
|
value={defaultModelValue}
|
||||||
|
defaultValue={defaultModelValue}
|
||||||
style={{ width: 360 }}
|
style={{ width: 360 }}
|
||||||
onChange={(id) => setDefaultModel(find(allModels, { id }) as Model)}
|
onChange={(value) => setDefaultModel(find(allModels, JSON.parse(value)) as Model)}
|
||||||
options={selectOptions}
|
options={selectOptions}
|
||||||
|
placeholder={t('settings.models.empty')}
|
||||||
/>
|
/>
|
||||||
<div style={{ height: 30 }} />
|
<div style={{ height: 30 }} />
|
||||||
<SettingTitle>
|
<SettingTitle>
|
||||||
@@ -53,10 +69,12 @@ const ModelSettings: FC = () => {
|
|||||||
</SettingTitle>
|
</SettingTitle>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<Select
|
<Select
|
||||||
defaultValue={topicNamingModel.id}
|
value={defaultTopicNamingModel}
|
||||||
|
defaultValue={defaultTopicNamingModel}
|
||||||
style={{ width: 360 }}
|
style={{ width: 360 }}
|
||||||
onChange={(id) => setTopicNamingModel(find(allModels, { id }) as Model)}
|
onChange={(value) => setTopicNamingModel(find(allModels, JSON.parse(value)) as Model)}
|
||||||
options={selectOptions}
|
options={selectOptions}
|
||||||
|
placeholder={t('settings.models.empty')}
|
||||||
/>
|
/>
|
||||||
<div style={{ height: 30 }} />
|
<div style={{ height: 30 }} />
|
||||||
<SettingTitle>
|
<SettingTitle>
|
||||||
@@ -67,9 +85,10 @@ const ModelSettings: FC = () => {
|
|||||||
</SettingTitle>
|
</SettingTitle>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<Select
|
<Select
|
||||||
defaultValue={translateModel?.id}
|
value={defaultTranslateModel}
|
||||||
|
defaultValue={defaultTranslateModel}
|
||||||
style={{ width: 360 }}
|
style={{ width: 360 }}
|
||||||
onChange={(id) => setTranslateModel(find(allModels, { id }) as Model)}
|
onChange={(value) => setTranslateModel(find(allModels, JSON.parse(value)) as Model)}
|
||||||
options={selectOptions}
|
options={selectOptions}
|
||||||
placeholder={t('settings.models.empty')}
|
placeholder={t('settings.models.empty')}
|
||||||
/>
|
/>
|
||||||
@@ -77,4 +96,6 @@ const ModelSettings: FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const iconStyle = { fontSize: 16, marginRight: 8 }
|
||||||
|
|
||||||
export default ModelSettings
|
export default ModelSettings
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ const PopupContainer: React.FC<Props> = ({ title, provider, resolve }) => {
|
|||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
afterClose={onClose}
|
afterClose={onClose}
|
||||||
footer={null}>
|
footer={null}
|
||||||
|
centered>
|
||||||
<Form
|
<Form
|
||||||
form={form}
|
form={form}
|
||||||
labelCol={{ flex: '110px' }}
|
labelCol={{ flex: '110px' }}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const PopupContainer: React.FC<Props> = ({ provider, resolve }) => {
|
|||||||
afterClose={onClose}
|
afterClose={onClose}
|
||||||
width={360}
|
width={360}
|
||||||
closable={false}
|
closable={false}
|
||||||
|
centered
|
||||||
title={t('settings.provider.edit.name')}
|
title={t('settings.provider.edit.name')}
|
||||||
okButtonProps={{ disabled: buttonDisabled }}>
|
okButtonProps={{ disabled: buttonDisabled }}>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { LoadingOutlined, MinusOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
import { LoadingOutlined, MinusOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||||
import { SYSTEM_MODELS } from '@renderer/config/models'
|
import VisionIcon from '@renderer/components/Icons/VisionIcon'
|
||||||
|
import { isVisionModel, SYSTEM_MODELS } from '@renderer/config/models'
|
||||||
import { getModelLogo } from '@renderer/config/provider'
|
import { getModelLogo } from '@renderer/config/provider'
|
||||||
import { useProvider } from '@renderer/hooks/useProvider'
|
import { useProvider } from '@renderer/hooks/useProvider'
|
||||||
import { fetchModels } from '@renderer/services/api'
|
import { fetchModels } from '@renderer/services/api'
|
||||||
@@ -107,7 +108,8 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
styles={{
|
styles={{
|
||||||
content: { padding: 0 },
|
content: { padding: 0 },
|
||||||
header: { padding: 22, paddingBottom: 15 }
|
header: { padding: 22, paddingBottom: 15 }
|
||||||
}}>
|
}}
|
||||||
|
centered>
|
||||||
<SearchContainer>
|
<SearchContainer>
|
||||||
<Search placeholder={t('settings.provider.search_placeholder')} allowClear onSearch={setSearchText} />
|
<Search placeholder={t('settings.provider.search_placeholder')} allowClear onSearch={setSearchText} />
|
||||||
</SearchContainer>
|
</SearchContainer>
|
||||||
@@ -125,6 +127,7 @@ const PopupContainer: React.FC<Props> = ({ provider: _provider, resolve }) => {
|
|||||||
</Avatar>
|
</Avatar>
|
||||||
<ListItemName>
|
<ListItemName>
|
||||||
{model.name}
|
{model.name}
|
||||||
|
{isVisionModel(model) && <VisionIcon />}
|
||||||
{isFreeModel(model) && (
|
{isFreeModel(model) && (
|
||||||
<Tag style={{ marginLeft: 10 }} color="green">
|
<Tag style={{ marginLeft: 10 }} color="green">
|
||||||
Free
|
Free
|
||||||
@@ -154,7 +157,8 @@ const onShowModelInfo = (model: Model) => {
|
|||||||
title: model.name,
|
title: model.name,
|
||||||
content: model?.description,
|
content: model?.description,
|
||||||
icon: null,
|
icon: null,
|
||||||
maskClosable: true
|
maskClosable: true,
|
||||||
|
width: 600
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ import {
|
|||||||
MinusCircleOutlined,
|
MinusCircleOutlined,
|
||||||
PlusOutlined
|
PlusOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
|
import VisionIcon from '@renderer/components/Icons/VisionIcon'
|
||||||
|
import { isVisionModel } from '@renderer/config/models'
|
||||||
import { getModelLogo } from '@renderer/config/provider'
|
import { getModelLogo } from '@renderer/config/provider'
|
||||||
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
import { PROVIDER_CONFIG } from '@renderer/config/provider'
|
||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { useProvider } from '@renderer/hooks/useProvider'
|
import { useProvider } from '@renderer/hooks/useProvider'
|
||||||
import { useTheme } from '@renderer/providers/ThemeProvider'
|
|
||||||
import { checkApi } from '@renderer/services/api'
|
import { checkApi } from '@renderer/services/api'
|
||||||
import { Provider } from '@renderer/types'
|
import { Provider } from '@renderer/types'
|
||||||
import { Avatar, Button, Card, Divider, Flex, Input, Space, Switch } from 'antd'
|
import { Avatar, Button, Card, Divider, Flex, Input, Space, Switch } from 'antd'
|
||||||
import Link from 'antd/es/typography/Link'
|
import Link from 'antd/es/typography/Link'
|
||||||
import { groupBy } from 'lodash'
|
import { groupBy, isEmpty } from 'lodash'
|
||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
@@ -72,7 +74,6 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
const docsWebsite = providerConfig?.websites?.docs
|
const docsWebsite = providerConfig?.websites?.docs
|
||||||
const modelsWebsite = providerConfig?.websites?.models
|
const modelsWebsite = providerConfig?.websites?.models
|
||||||
const configedApiHost = providerConfig?.api?.url
|
const configedApiHost = providerConfig?.api?.url
|
||||||
const apiEditable = provider.isSystem ? providerConfig?.api?.editable : true
|
|
||||||
|
|
||||||
const onReset = () => {
|
const onReset = () => {
|
||||||
setApiHost(configedApiHost)
|
setApiHost(configedApiHost)
|
||||||
@@ -131,9 +132,10 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
placeholder={t('settings.provider.api_host')}
|
placeholder={t('settings.provider.api_host')}
|
||||||
onChange={(e) => setApiHost(e.target.value)}
|
onChange={(e) => setApiHost(e.target.value)}
|
||||||
onBlur={onUpdateApiHost}
|
onBlur={onUpdateApiHost}
|
||||||
disabled={!apiEditable}
|
|
||||||
/>
|
/>
|
||||||
{apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>}
|
{!isEmpty(configedApiHost) && apiHost !== configedApiHost && (
|
||||||
|
<Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>
|
||||||
|
)}
|
||||||
</Space.Compact>
|
</Space.Compact>
|
||||||
{provider.id === 'ollama' && <OllamSettings />}
|
{provider.id === 'ollama' && <OllamSettings />}
|
||||||
{provider.id === 'graphrag-kylin-mountain' && provider.models.length > 0 && (
|
{provider.id === 'graphrag-kylin-mountain' && provider.models.length > 0 && (
|
||||||
@@ -148,7 +150,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
|
|||||||
<Avatar src={getModelLogo(model.id)} size={22} style={{ marginRight: '8px' }}>
|
<Avatar src={getModelLogo(model.id)} size={22} style={{ marginRight: '8px' }}>
|
||||||
{model.name[0].toUpperCase()}
|
{model.name[0].toUpperCase()}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
{model.name}
|
{model.name} {isVisionModel(model) && <VisionIcon />}
|
||||||
</ModelListHeader>
|
</ModelListHeader>
|
||||||
<RemoveIcon onClick={() => removeModel(model)} />
|
<RemoveIcon onClick={() => removeModel(model)} />
|
||||||
</ModelListItem>
|
</ModelListItem>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PlusOutlined } from '@ant-design/icons'
|
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
import { DeleteOutlined, EditOutlined } from '@ant-design/icons'
|
|
||||||
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
import { DragDropContext, Draggable, Droppable, DropResult } from '@hello-pangea/dnd'
|
||||||
import { getProviderLogo } from '@renderer/config/provider'
|
import { getProviderLogo } from '@renderer/config/provider'
|
||||||
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
|
import { useAllProviders, useProviders } from '@renderer/hooks/useProvider'
|
||||||
@@ -72,6 +71,7 @@ const ProvidersList: FC = () => {
|
|||||||
content: t('settings.provider.delete.content'),
|
content: t('settings.provider.delete.content'),
|
||||||
okButtonProps: { danger: true },
|
okButtonProps: { danger: true },
|
||||||
okText: t('common.delete'),
|
okText: t('common.delete'),
|
||||||
|
centered: true,
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
setSelectedProvider(providers.filter((p) => p.isSystem)[0])
|
setSelectedProvider(providers.filter((p) => p.isSystem)[0])
|
||||||
removeProvider(provider)
|
removeProvider(provider)
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { CloudOutlined, InfoCircleOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons'
|
||||||
CloudOutlined,
|
|
||||||
CodeSandboxOutlined,
|
|
||||||
InfoCircleOutlined,
|
|
||||||
MessageOutlined,
|
|
||||||
SettingOutlined
|
|
||||||
} from '@ant-design/icons'
|
|
||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||||
import { isLocalAi } from '@renderer/config/env'
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
@@ -41,7 +35,7 @@ const SettingsPage: FC = () => {
|
|||||||
</MenuItemLink>
|
</MenuItemLink>
|
||||||
<MenuItemLink to="/settings/model">
|
<MenuItemLink to="/settings/model">
|
||||||
<MenuItem className={isRoute('/settings/model')}>
|
<MenuItem className={isRoute('/settings/model')}>
|
||||||
<CodeSandboxOutlined />
|
<i className="iconfont icon-ai-model" />
|
||||||
{t('settings.model')}
|
{t('settings.model')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuItemLink>
|
</MenuItemLink>
|
||||||
@@ -96,7 +90,7 @@ const ContentContainer = styled.div`
|
|||||||
const SettingMenus = styled.ul`
|
const SettingMenus = styled.ul`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-width: var(--assistants-width);
|
min-width: var(--settings-width);
|
||||||
border-right: 0.5px solid var(--color-border);
|
border-right: 0.5px solid var(--color-border);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
`
|
`
|
||||||
@@ -122,6 +116,11 @@ const MenuItem = styled.li`
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
.iconfont {
|
||||||
|
font-size: 18px;
|
||||||
|
opacity: 0.7;
|
||||||
|
margin-left: -1px;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-background-soft);
|
background: var(--color-background-soft);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { Divider } from 'antd'
|
|||||||
import Link from 'antd/es/typography/Link'
|
import Link from 'antd/es/typography/Link'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import SettingsPage from './SettingsPage'
|
|
||||||
|
|
||||||
export const SettingContainer = styled.div`
|
export const SettingContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -69,5 +67,3 @@ export const SettingHelpLink = styled(Link)`
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default SettingsPage
|
|
||||||
|
|||||||
40
src/renderer/src/providers/AiProvider.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import BaseProvider from '@renderer/providers/BaseProvider'
|
||||||
|
import ProviderFactory from '@renderer/providers/ProviderFactory'
|
||||||
|
import { Assistant, Message, Provider, Suggestion } from '@renderer/types'
|
||||||
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
|
export default class AiProvider {
|
||||||
|
private sdk: BaseProvider
|
||||||
|
|
||||||
|
constructor(provider: Provider) {
|
||||||
|
this.sdk = ProviderFactory.create(provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams): Promise<void> {
|
||||||
|
return this.sdk.completions({ messages, assistant, onChunk, onFilterMessages })
|
||||||
|
}
|
||||||
|
|
||||||
|
public async translate(message: Message, assistant: Assistant): Promise<string> {
|
||||||
|
return this.sdk.translate(message, assistant)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async summaries(messages: Message[], assistant: Assistant): Promise<string> {
|
||||||
|
return this.sdk.summaries(messages, assistant)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async suggestions(messages: Message[], assistant: Assistant): Promise<Suggestion[]> {
|
||||||
|
return this.sdk.suggestions(messages, assistant)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {
|
||||||
|
return this.sdk.generateText({ prompt, content })
|
||||||
|
}
|
||||||
|
|
||||||
|
public async check(): Promise<{ valid: boolean; error: Error | null }> {
|
||||||
|
return this.sdk.check()
|
||||||
|
}
|
||||||
|
|
||||||
|
public async models(): Promise<OpenAI.Models.Model[]> {
|
||||||
|
return this.sdk.models()
|
||||||
|
}
|
||||||
|
}
|
||||||
201
src/renderer/src/providers/AnthropicProvider.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import Anthropic from '@anthropic-ai/sdk'
|
||||||
|
import { MessageCreateParamsNonStreaming, MessageParam } from '@anthropic-ai/sdk/resources'
|
||||||
|
import { DEFAULT_MAX_TOKENS } from '@renderer/config/constant'
|
||||||
|
import { getAssistantSettings, getDefaultModel, getTopNamingModel } from '@renderer/services/assistant'
|
||||||
|
import { EVENT_NAMES } from '@renderer/services/event'
|
||||||
|
import { filterContextMessages, filterMessages } from '@renderer/services/messages'
|
||||||
|
import { Assistant, FileTypes, Message, Provider, Suggestion } from '@renderer/types'
|
||||||
|
import { first, flatten, sum, takeRight } from 'lodash'
|
||||||
|
import OpenAI from 'openai'
|
||||||
|
|
||||||
|
import BaseProvider from './BaseProvider'
|
||||||
|
|
||||||
|
export default class AnthropicProvider extends BaseProvider {
|
||||||
|
private sdk: Anthropic
|
||||||
|
|
||||||
|
constructor(provider: Provider) {
|
||||||
|
super(provider)
|
||||||
|
this.sdk = new Anthropic({ apiKey: provider.apiKey, baseURL: this.getBaseURL() })
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getMessageParam(message: Message): Promise<MessageParam> {
|
||||||
|
const parts: MessageParam['content'] = [{ type: 'text', text: message.content }]
|
||||||
|
|
||||||
|
for (const file of message.files || []) {
|
||||||
|
if (file.type === FileTypes.IMAGE) {
|
||||||
|
const base64Data = await window.api.file.base64Image(file.id + file.ext)
|
||||||
|
parts.push({
|
||||||
|
type: 'image',
|
||||||
|
source: {
|
||||||
|
data: base64Data.base64,
|
||||||
|
media_type: base64Data.mime.replace('jpg', 'jpeg') as any,
|
||||||
|
type: 'base64'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (file.type === FileTypes.TEXT) {
|
||||||
|
const fileContent = await (await window.api.file.read(file.id + file.ext)).trim()
|
||||||
|
parts.push({
|
||||||
|
type: 'text',
|
||||||
|
text: file.origin_name + '\n' + fileContent
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: message.role,
|
||||||
|
content: parts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async completions({ messages, assistant, onChunk, onFilterMessages }: CompletionsParams) {
|
||||||
|
const defaultModel = getDefaultModel()
|
||||||
|
const model = assistant.model || defaultModel
|
||||||
|
const { contextCount, maxTokens } = getAssistantSettings(assistant)
|
||||||
|
|
||||||
|
const userMessagesParams: MessageParam[] = []
|
||||||
|
const _messages = filterMessages(filterContextMessages(takeRight(messages, contextCount + 2)))
|
||||||
|
|
||||||
|
onFilterMessages(_messages)
|
||||||
|
|
||||||
|
for (const message of _messages) {
|
||||||
|
userMessagesParams.push(await this.getMessageParam(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMessages = flatten(userMessagesParams)
|
||||||
|
|
||||||
|
if (first(userMessages)?.role === 'assistant') {
|
||||||
|
userMessages.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
const stream = this.sdk.messages
|
||||||
|
.stream({
|
||||||
|
model: model.id,
|
||||||
|
messages: userMessages,
|
||||||
|
max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
|
||||||
|
temperature: assistant?.settings?.temperature,
|
||||||
|
system: assistant.prompt,
|
||||||
|
stream: true
|
||||||
|
})
|
||||||
|
.on('text', (text) => {
|
||||||
|
if (window.keyv.get(EVENT_NAMES.CHAT_COMPLETION_PAUSED)) {
|
||||||
|
resolve()
|
||||||
|
return stream.controller.abort()
|
||||||
|
}
|
||||||
|
onChunk({ text })
|
||||||
|
})
|
||||||
|
.on('finalMessage', (message) => {
|
||||||
|
onChunk({
|
||||||
|
text: '',
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: message.usage.input_tokens,
|
||||||
|
completion_tokens: message.usage.output_tokens,
|
||||||
|
total_tokens: sum(Object.values(message.usage))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
.on('error', (error) => reject(error))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async translate(message: Message, assistant: Assistant) {
|
||||||
|
const defaultModel = getDefaultModel()
|
||||||
|
const model = assistant.model || defaultModel
|
||||||
|
const messages = [
|
||||||
|
{ role: 'system', content: assistant.prompt },
|
||||||
|
{ role: 'user', content: message.content }
|
||||||
|
]
|
||||||
|
|
||||||
|
const response = await this.sdk.messages.create({
|
||||||
|
model: model.id,
|
||||||
|
messages: messages.filter((m) => m.role === 'user') as MessageParam[],
|
||||||
|
max_tokens: 4096,
|
||||||
|
temperature: assistant?.settings?.temperature,
|
||||||
|
system: assistant.prompt,
|
||||||
|
stream: false
|
||||||
|
})
|
||||||
|
|
||||||
|
return response.content[0].type === 'text' ? response.content[0].text : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
public async summaries(messages: Message[], assistant: Assistant): Promise<string> {
|
||||||
|
const model = getTopNamingModel() || assistant.model || getDefaultModel()
|
||||||
|
|
||||||
|
const userMessages = takeRight(messages, 5).map((message) => ({
|
||||||
|
role: message.role,
|
||||||
|
content: message.content
|
||||||
|
}))
|
||||||
|
|
||||||
|
if (first(userMessages)?.role === 'assistant') {
|
||||||
|
userMessages.shift()
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemMessage = {
|
||||||
|
role: 'system',
|
||||||
|
content: '你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,不要使用标点符号和其他特殊符号。'
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = await this.sdk.messages.create({
|
||||||
|
messages: userMessages as Anthropic.Messages.MessageParam[],
|
||||||
|
model: model.id,
|
||||||
|
system: systemMessage.content,
|
||||||
|
stream: false,
|
||||||
|
max_tokens: 4096
|
||||||
|
})
|
||||||
|
|
||||||
|
return message.content[0].type === 'text' ? message.content[0].text : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
public async generateText({ prompt, content }: { prompt: string; content: string }): Promise<string> {
|
||||||
|
const model = getDefaultModel()
|
||||||
|
|
||||||
|
const message = await this.sdk.messages.create({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content
|
||||||
|
}
|
||||||
|
],
|
||||||
|
model: model.id,
|
||||||
|
system: prompt,
|
||||||
|
stream: false,
|
||||||
|
max_tokens: 4096
|
||||||
|
})
|
||||||
|
|
||||||
|
return message.content[0].type === 'text' ? message.content[0].text : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
public async suggestions(): Promise<Suggestion[]> {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
public async check(): Promise<{ valid: boolean; error: Error | null }> {
|
||||||
|
const model = this.provider.models[0]
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
model: model.id,
|
||||||
|
messages: [{ role: 'user', content: 'hi' }],
|
||||||
|
max_tokens: 100,
|
||||||
|
stream: false
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const message = await this.sdk.messages.create(body as MessageCreateParamsNonStreaming)
|
||||||
|
return {
|
||||||
|
valid: message.content.length > 0,
|
||||||
|
error: null
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async models(): Promise<OpenAI.Models.Model[]> {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||