Compare commits

...

27 Commits

Author SHA1 Message Date
kangfenmao
a2dc325896 chore(version): 0.6.1 2024-08-22 19:17:35 +08:00
kangfenmao
b131d320ea feat: more ai minapp 2024-08-22 18:45:06 +08:00
kangfenmao
b88f4a869e wip 2024-08-22 16:36:04 +08:00
kangfenmao
461458e5ec refactor: remove minapp.html 2024-08-22 13:04:24 +08:00
kangfenmao
4c2014f1d6 chore(version): 0.6.0 2024-08-21 10:28:31 +08:00
kangfenmao
647dd3e751 feat: add minapps 2024-08-21 10:14:04 +08:00
kangfenmao
4225312d4a chore(version): 0.5.9 2024-08-20 13:42:50 +08:00
kangfenmao
c2a4613e32 fix: windows minapp control button 2024-08-18 23:37:09 +08:00
kangfenmao
5d5c1eee74 feat: change sidebar width 2024-08-18 22:20:09 +08:00
kangfenmao
c1b5e6b183 feat: new input status bar style 2024-08-18 20:44:55 +08:00
kangfenmao
fd37ba18dc chore(version): 0.5.8 2024-08-18 18:06:56 +08:00
kangfenmao
4a26f7ce78 feat: add minimax provider 2024-08-18 18:06:21 +08:00
kangfenmao
8b38ebcac4 fix: hmr recycle 2024-08-18 17:10:59 +08:00
kangfenmao
e8dac28787 fix: graph rag model id 2024-08-17 21:54:34 +08:00
kangfenmao
3ccebb503f fix: input text 2024-08-17 21:30:28 +08:00
kangfenmao
42327836de fix: graphrag node url 2024-08-17 21:30:04 +08:00
kangfenmao
4d7a3bb8c3 feat: add minapp window 2024-08-17 17:11:48 +08:00
kangfenmao
1996e163c9 feat: add minapp window 2024-08-17 13:30:54 +08:00
kangfenmao
e43f7f87ab feat: window.app add app path 2024-08-16 22:44:00 +08:00
kangfenmao
47a83fa67f fix: minapp title null 2024-08-16 22:43:18 +08:00
kangfenmao
5e954566c9 chore(version): 0.5.7 2024-08-16 17:41:30 +08:00
kangfenmao
b8960ef02c fix: windows frame background color 2024-08-16 17:41:14 +08:00
kangfenmao
1866b00265 feat: add user edit modal & add prompt block 2024-08-16 17:19:18 +08:00
kangfenmao
be0799a4c6 chore(version): 0.5.6 2024-08-14 21:32:14 +08:00
kangfenmao
d0f5547419 feat: new windows and linux sidebar style 2024-08-14 21:28:44 +08:00
kangfenmao
076011b02b fix: anthropic message generating error 2024-08-14 20:35:57 +08:00
kangfenmao
ba5c70c45a feat: add minapp popup 2024-08-14 19:47:58 +08:00
94 changed files with 1648 additions and 381 deletions

View File

@@ -56,4 +56,5 @@ electronDownload:
afterSign: scripts/notarize.js afterSign: scripts/notarize.js
releaseInfo: releaseInfo:
releaseNotes: | releaseNotes: |
增加 Gemini、豆包、阶跃星辰等服务商支持 增加应用备份和恢复功能
增加更多AI小程序

View File

@@ -1,6 +1,6 @@
{ {
"name": "cherry-studio", "name": "cherry-studio",
"version": "0.5.5", "version": "0.6.1",
"description": "A powerful AI assistant for producer.", "description": "A powerful AI assistant for producer.",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "kangfenmao@qq.com", "author": "kangfenmao@qq.com",

68
resources/graphrag.html Normal file
View File

@@ -0,0 +1,68 @@
<head>
<style>
body {
margin: 0;
}
</style>
<script src="https://unpkg.com/3d-force-graph"></script>
</head>
<body>
<div id="3d-graph"></div>
<script src="./js/bridge.js"></script>
<script type="module">
import { getQueryParam } from './js/utils.js'
const apiUrl = getQueryParam('apiUrl')
const modelId = getQueryParam('modelId')
const jsonUrl = `${apiUrl}/v1/global_graph/${modelId}`
const infoCard = document.createElement('div')
infoCard.style.position = 'fixed'
infoCard.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'
infoCard.style.padding = '8px'
infoCard.style.borderRadius = '4px'
infoCard.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)'
infoCard.style.fontSize = '12px'
infoCard.style.maxWidth = '200px'
infoCard.style.display = 'none'
infoCard.style.zIndex = '1000'
document.body.appendChild(infoCard)
document.addEventListener('mousemove', (event) => {
infoCard.style.left = `${event.clientX + 10}px`
infoCard.style.top = `${event.clientY + 10}px`
})
const elem = document.getElementById('3d-graph')
const Graph = ForceGraph3D()(elem)
.jsonUrl(jsonUrl)
.nodeAutoColorBy((node) => node.properties.type || 'default')
.nodeVal((node) => node.properties.degree)
.linkWidth((link) => link.properties.weight)
.onNodeHover((node) => {
if (node) {
infoCard.innerHTML = `
<div style="font-weight: bold; margin-bottom: 4px; color: #333;">
${node.properties.title}
</div>
<div style="color: #666;">
${node.properties.description}
</div>`
infoCard.style.display = 'block'
} else {
infoCard.style.display = 'none'
}
})
.onNodeClick((node) => {
const url = `${apiUrl}/v1/references/${modelId}/entities/${node.properties.human_readable_id}`
window.api.minApp({
url,
windowOptions: {
title: node.properties.title,
width: 500,
height: 800
}
})
})
</script>
</body>

36
resources/js/bridge.js Normal file
View File

@@ -0,0 +1,36 @@
;(() => {
let messageId = 0
const pendingCalls = new Map()
function api(method, ...args) {
const id = messageId++
return new Promise((resolve, reject) => {
pendingCalls.set(id, { resolve, reject })
window.parent.postMessage({ id, type: 'api-call', method, args }, '*')
})
}
window.addEventListener('message', (event) => {
if (event.data.type === 'api-response') {
const { id, result, error } = event.data
const pendingCall = pendingCalls.get(id)
if (pendingCall) {
if (error) {
pendingCall.reject(new Error(error))
} else {
pendingCall.resolve(result)
}
pendingCalls.delete(id)
}
}
})
window.api = new Proxy(
{},
{
get: (target, prop) => {
return (...args) => api(prop, ...args)
}
}
)
})()

5
resources/js/utils.js Normal file
View File

@@ -0,0 +1,5 @@
export function getQueryParam(paramName) {
const url = new URL(window.location.href)
const params = new URLSearchParams(url.search)
return params.get(paramName)
}

View File

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

View File

@@ -1,84 +1,10 @@
import { electronApp, is, optimizer } from '@electron-toolkit/utils' import { electronApp, optimizer } from '@electron-toolkit/utils'
import * as Sentry from '@sentry/electron/main' import * as Sentry from '@sentry/electron/main'
import { app, BrowserWindow, ipcMain, Menu, MenuItem, session, shell } from 'electron' import { app, BrowserWindow } from 'electron'
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer' import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
import icon from '../../build/icon.png?asset' import { registerIpc } from './ipc'
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config' import { createMainWindow } from './window'
import { saveFile } from './event'
import AppUpdater from './updater'
function createWindow() {
// Load the previous state with fallback to defaults
const mainWindowState = windowStateKeeper({
defaultWidth: 1080,
defaultHeight: 670
})
const theme = appConfig.get('theme') || 'light'
// Create the browser window.
const mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 1080,
minHeight: 600,
show: true,
autoHideMenuBar: true,
transparent: process.platform === 'darwin',
vibrancy: 'fullscreen-ui',
titleBarStyle: 'hidden',
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
trafficLightPosition: { x: 8, y: 12 },
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false
// devTools: !app.isPackaged,
}
})
mainWindowState.manage(mainWindow)
mainWindow.webContents.on('context-menu', () => {
const menu = new Menu()
menu.append(new MenuItem({ label: '复制', role: 'copy', sublabel: '⌘ + C' }))
menu.append(new MenuItem({ label: '粘贴', role: 'paste', sublabel: '⌘ + V' }))
menu.append(new MenuItem({ label: '剪切', role: 'cut', sublabel: '⌘ + X' }))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({ label: '全选', role: 'selectAll', sublabel: '⌘ + A' }))
menu.popup()
})
mainWindow.webContents.on('will-navigate', (event, url) => {
event.preventDefault()
shell.openExternal(url)
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
return mainWindow
}
// This method will be called when Electron has finished // This method will be called when Electron has finished
// initialization and is ready to create browser windows. // initialization and is ready to create browser windows.
@@ -97,43 +23,12 @@ app.whenReady().then(() => {
app.on('activate', function () { app.on('activate', function () {
// On macOS it's common to re-create a window in the app when the // On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open. // dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow() if (BrowserWindow.getAllWindows().length === 0) createMainWindow()
}) })
const mainWindow = createWindow() const mainWindow = createMainWindow()
const { autoUpdater } = new AppUpdater(mainWindow) registerIpc(mainWindow, app)
// IPC
ipcMain.handle('get-app-info', () => ({
version: app.getVersion(),
isPackaged: app.isPackaged
}))
ipcMain.handle('open-website', (_, url: string) => {
shell.openExternal(url)
})
ipcMain.handle('set-proxy', (_, proxy: string) => {
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
})
ipcMain.handle('save-file', saveFile)
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
appConfig.set('theme', theme)
mainWindow?.setTitleBarOverlay &&
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
})
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.handle('check-for-update', async () => {
autoUpdater.logger?.info('触发检查更新')
return {
currentVersion: autoUpdater.currentVersion,
update: await autoUpdater.checkForUpdates()
}
})
installExtension(REDUX_DEVTOOLS) installExtension(REDUX_DEVTOOLS)
}) })

59
src/main/ipc.ts Normal file
View File

@@ -0,0 +1,59 @@
import { BrowserWindow, ipcMain, session, shell } from 'electron'
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
import AppUpdater from './updater'
import { openFile, saveFile } from './utils/file'
import { compress, decompress } from './utils/zip'
import { createMinappWindow } from './window'
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const { autoUpdater } = new AppUpdater(mainWindow)
// IPC
ipcMain.handle('get-app-info', () => ({
version: app.getVersion(),
isPackaged: app.isPackaged,
appPath: app.getAppPath()
}))
ipcMain.handle('open-website', (_, url: string) => {
shell.openExternal(url)
})
ipcMain.handle('set-proxy', (_, proxy: string) => {
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
})
ipcMain.handle('save-file', saveFile)
ipcMain.handle('open-file', openFile)
ipcMain.handle('reload', () => mainWindow.reload())
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
ipcMain.handle('minapp', (_, args) => {
createMinappWindow({
url: args.url,
parent: mainWindow,
windowOptions: {
...mainWindow.getBounds(),
...args.windowOptions
}
})
})
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
appConfig.set('theme', theme)
mainWindow?.setTitleBarOverlay &&
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
})
// 触发检查更新(此方法用于被渲染线程调用,例如页面点击检查更新按钮来调用此方法)
ipcMain.handle('check-for-update', async () => {
autoUpdater.logger?.info('触发检查更新')
return {
currentVersion: autoUpdater.currentVersion,
update: await autoUpdater.checkForUpdates()
}
})
}

24
src/main/utils/aes.ts Normal file
View File

@@ -0,0 +1,24 @@
import * as crypto from 'crypto'
// 定义密钥和初始化向量IV
const secretKey = 'kDQvWz5slot3syfucoo53X6KKsEUJoeFikpiUWRJTLIo3zcUPpFvEa009kK13KCr'
const iv = Buffer.from('Cherry Studio', 'hex')
// 加密函数
export function encrypt(text: string): { iv: string; encryptedData: string } {
const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(secretKey), iv)
let encrypted = cipher.update(text, 'utf8', 'hex')
encrypted += cipher.final('hex')
return {
iv: iv.toString('hex'),
encryptedData: encrypted
}
}
// 解密函数
export function decrypt(encryptedData: string, iv: string): string {
const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(secretKey), Buffer.from(iv, 'hex'))
let decrypted = decipher.update(encryptedData, 'hex', 'utf8')
decrypted += decipher.final('utf8')
return decrypted
}

55
src/main/utils/file.ts Normal file
View File

@@ -0,0 +1,55 @@
import { dialog, OpenDialogOptions, OpenDialogReturnValue, SaveDialogOptions, SaveDialogReturnValue } from 'electron'
import logger from 'electron-log'
import { writeFile } from 'fs'
import { readFile } from 'fs/promises'
export async function saveFile(
_: Electron.IpcMainInvokeEvent,
fileName: string,
content: string,
options?: SaveDialogOptions
): Promise<void> {
try {
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
title: '保存文件',
defaultPath: fileName,
...options
})
if (!result.canceled && result.filePath) {
writeFile(result.filePath, content, { encoding: 'utf-8' }, (err) => {
if (err) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
}
})
}
} catch (err) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
}
}
export async function openFile(
_: Electron.IpcMainInvokeEvent,
options: OpenDialogOptions
): Promise<{ fileName: string; content: Buffer } | null> {
try {
const result: OpenDialogReturnValue = await dialog.showOpenDialog({
title: '打开文件',
properties: ['openFile'],
filters: [{ name: '所有文件', extensions: ['*'] }],
...options
})
if (!result.canceled && result.filePaths.length > 0) {
const filePath = result.filePaths[0]
const fileName = filePath.split('/').pop() || ''
const content = await readFile(filePath)
return { fileName, content }
}
return null
} catch (err) {
logger.error('[IPC - Error]', 'An error occurred opening the file:', err)
return null
}
}

37
src/main/utils/zip.ts Normal file
View File

@@ -0,0 +1,37 @@
import util from 'node:util'
import zlib from 'node:zlib'
// 将 zlib 的 gzip 和 gunzip 方法转换为 Promise 版本
const gzipPromise = util.promisify(zlib.gzip)
const gunzipPromise = util.promisify(zlib.gunzip)
/**
* 压缩字符串
* @param {string} string - 要压缩的 JSON 字符串
* @returns {Promise<Buffer>} 压缩后的 Buffer
*/
export async function compress(str) {
try {
const buffer = Buffer.from(str, 'utf-8')
const compressedBuffer = await gzipPromise(buffer)
return compressedBuffer
} catch (error) {
console.error('Compression failed:', error)
throw error
}
}
/**
* 解压缩 Buffer 到 JSON 字符串
* @param {Buffer} compressedBuffer - 压缩的 Buffer
* @returns {Promise<string>} 解压缩后的 JSON 字符串
*/
export async function decompress(compressedBuffer) {
try {
const buffer = await gunzipPromise(compressedBuffer)
return buffer.toString('utf-8')
} catch (error) {
console.error('Decompression failed:', error)
throw error
}
}

124
src/main/window.ts Normal file
View File

@@ -0,0 +1,124 @@
import { is } from '@electron-toolkit/utils'
import { BrowserWindow, Menu, MenuItem, shell } from 'electron'
import windowStateKeeper from 'electron-window-state'
import { join } from 'path'
import icon from '../../build/icon.png?asset'
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
export function createMainWindow() {
// Load the previous state with fallback to defaults
const mainWindowState = windowStateKeeper({
defaultWidth: 1080,
defaultHeight: 670
})
const theme = appConfig.get('theme') || 'light'
// Create the browser window.
const mainWindow = new BrowserWindow({
x: mainWindowState.x,
y: mainWindowState.y,
width: mainWindowState.width,
height: mainWindowState.height,
minWidth: 1080,
minHeight: 600,
show: true,
autoHideMenuBar: true,
transparent: process.platform === 'darwin',
vibrancy: 'fullscreen-ui',
titleBarStyle: 'hidden',
titleBarOverlay: theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight,
trafficLightPosition: { x: 8, y: 12 },
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: false
// devTools: !app.isPackaged,
}
})
mainWindowState.manage(mainWindow)
mainWindow.webContents.on('context-menu', () => {
const menu = new Menu()
menu.append(new MenuItem({ label: '复制', role: 'copy', sublabel: '⌘ + C' }))
menu.append(new MenuItem({ label: '粘贴', role: 'paste', sublabel: '⌘ + V' }))
menu.append(new MenuItem({ label: '剪切', role: 'cut', sublabel: '⌘ + X' }))
menu.append(new MenuItem({ type: 'separator' }))
menu.append(new MenuItem({ label: '全选', role: 'selectAll', sublabel: '⌘ + A' }))
menu.popup()
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.on('will-navigate', (event, url) => {
event.preventDefault()
shell.openExternal(url)
})
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url)
return { action: 'deny' }
})
mainWindow.webContents.session.webRequest.onHeadersReceived({ urls: ['*://*/*'] }, (details, callback) => {
if (details.responseHeaders?.['X-Frame-Options']) {
delete details.responseHeaders['X-Frame-Options']
}
if (details.responseHeaders?.['x-frame-options']) {
delete details.responseHeaders['x-frame-options']
}
if (details.responseHeaders?.['Content-Security-Policy']) {
delete details.responseHeaders['Content-Security-Policy']
}
if (details.responseHeaders?.['content-security-policy']) {
delete details.responseHeaders['content-security-policy']
}
callback({ cancel: false, responseHeaders: details.responseHeaders })
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
return mainWindow
}
export function createMinappWindow({
url,
parent,
windowOptions
}: {
url: string
parent?: BrowserWindow
windowOptions?: Electron.BrowserWindowConstructorOptions
}) {
const width = windowOptions?.width || 1000
const height = windowOptions?.height || 680
const minappWindow = new BrowserWindow({
width,
height,
autoHideMenuBar: true,
title: 'Cherry Studio',
...windowOptions,
parent,
webPreferences: {
preload: join(__dirname, '../preload/minapp.js'),
sandbox: false,
contextIsolation: false
}
})
minappWindow.loadURL(url)
return minappWindow
}

View File

@@ -1,4 +1,5 @@
import { ElectronAPI } from '@electron-toolkit/preload' import { ElectronAPI } from '@electron-toolkit/preload'
import type { OpenDialogOptions } from 'electron'
declare global { declare global {
interface Window { interface Window {
@@ -7,12 +8,18 @@ declare global {
getAppInfo: () => Promise<{ getAppInfo: () => Promise<{
version: string version: string
isPackaged: boolean isPackaged: boolean
appPath: string
}> }>
checkForUpdate: () => void checkForUpdate: () => void
openWebsite: (url: string) => void openWebsite: (url: string) => void
setProxy: (proxy: string | undefined) => void setProxy: (proxy: string | undefined) => void
saveFile: (path: string, content: string) => void saveFile: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void
openFile: (options?: OpenDialogOptions) => Promise<{ fileName: string; content: Buffer } | null>
setTheme: (theme: 'light' | 'dark') => void setTheme: (theme: 'light' | 'dark') => void
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
reload: () => void
compress: (text: string) => Promise<Buffer>
decompress: (text: Buffer) => Promise<string>
} }
} }
} }

View File

@@ -7,8 +7,15 @@ const api = {
checkForUpdate: () => ipcRenderer.invoke('check-for-update'), checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url), openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy), setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
saveFile: (path: string, content: string) => ipcRenderer.invoke('save-file', path, content), setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme),
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme) minApp: (url: string) => ipcRenderer.invoke('minapp', url),
openFile: (options?: { decompress: boolean }) => ipcRenderer.invoke('open-file', options),
reload: () => ipcRenderer.invoke('reload'),
saveFile: (path: string, content: string, options?: { compress: boolean }) => {
ipcRenderer.invoke('save-file', path, content, options)
},
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
} }
// Use `contextBridge` APIs to expose Electron APIs to // Use `contextBridge` APIs to expose Electron APIs to

View File

@@ -6,7 +6,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:" /> 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:" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -6,6 +6,7 @@ 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 AgentsPage from './pages/agents/AgentsPage' import AgentsPage from './pages/agents/AgentsPage'
import AppsPage from './pages/apps/AppsPage'
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'
@@ -23,8 +24,9 @@ function App(): JSX.Element {
<Sidebar /> <Sidebar />
<Routes> <Routes>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/apps" element={<AgentsPage />} /> <Route path="/agents" element={<AgentsPage />} />
<Route path="/translate" element={<TranslatePage />} /> <Route path="/translate" element={<TranslatePage />} />
<Route path="/apps" element={<AppsPage />} />
<Route path="/settings/*" element={<SettingsPage />} /> <Route path="/settings/*" element={<SettingsPage />} />
</Routes> </Routes>
</HashRouter> </HashRouter>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,7 +0,0 @@
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="300" cy="300" r="300" fill="white"/>
<rect x="409.733" y="340.032" width="42.3862" height="151.648" rx="21.1931" fill="#003425"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M422.005 133.354C413.089 125.771 399.714 126.851 392.131 135.768L273.699 275.021C270.643 278.614 268.994 282.932 268.698 287.302C268.532 288.371 268.446 289.466 268.446 290.581V468.603C268.446 480.308 277.934 489.796 289.639 489.796C301.344 489.796 310.832 480.308 310.832 468.603V296.784L424.419 163.228C432.002 154.312 430.921 140.937 422.005 133.354Z" fill="#003425"/>
<rect x="113.972" y="134.25" width="42.3862" height="174.745" rx="21.1931" transform="rotate(-39.3441 113.972 134.25)" fill="#003425"/>
<circle cx="460.126" cy="279.278" r="25.9027" fill="#00DD20"/>
</svg>

Before

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,7 +0,0 @@
<svg width="600" height="600" viewBox="0 0 600 600" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="300" cy="300" r="300" fill="#003425"/>
<rect x="409.733" y="340.031" width="42.3862" height="151.648" rx="21.1931" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M422.005 133.354C413.089 125.771 399.714 126.851 392.131 135.767L273.699 275.021C270.643 278.614 268.994 282.932 268.698 287.302C268.532 288.371 268.446 289.466 268.446 290.581V468.603C268.446 480.308 277.934 489.796 289.639 489.796C301.344 489.796 310.832 480.308 310.832 468.603V296.784L424.419 163.228C432.002 154.312 430.921 140.937 422.005 133.354Z" fill="white"/>
<rect x="113.972" y="134.25" width="42.3862" height="174.745" rx="21.1931" transform="rotate(-39.3441 113.972 134.25)" fill="white"/>
<circle cx="460.126" cy="279.278" r="25.9027" fill="#00FF25"/>
</svg>

Before

Width:  |  Height:  |  Size: 865 B

View File

@@ -1,7 +1,7 @@
@import './markdown.scss'; @import './markdown.scss';
@import './scrollbar.scss'; @import './scrollbar.scss';
@import '../fonts/icon-fonts/iconfont.css'; @import '../fonts/icon-fonts/iconfont.css';
@import '../fonts/Ubuntu/Ubuntu.css'; @import '../fonts/ubuntu/ubuntu.css';
:root { :root {
--color-white: #ffffff; --color-white: #ffffff;
@@ -31,24 +31,26 @@
--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: #ffffff20; --color-border: #000;
--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.15);
--color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3); --color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.3);
--navbar-background: rgba(30, 30, 30, 0.75); --navbar-background-mac: rgba(30, 30, 30, 0.8);
--sidebar-background: rgba(30, 30, 30, 0.75); --navbar-background: rgba(30, 30, 30);
--input-bar-background: rgba(255, 255, 255, 0.02); --input-bar-background: rgba(255, 255, 255, 0.02);
--navbar-height: 42px; --navbar-height: 42px;
--sidebar-width: 55px; --sidebar-width: 52px;
--status-bar-height: 40px;
--input-bar-height: 85px;
--assistants-width: 245px; --assistants-width: 245px;
--topic-list-width: 260px; --topic-list-width: 260px;
--settings-width: var(--assistants-width); --settings-width: var(--assistants-width);
--status-bar-height: 40px;
--input-bar-height: 85px;
} }
body[theme-mode='light'] { body[theme-mode='light'] {
@@ -80,14 +82,15 @@ body[theme-mode='light'] {
--color-icon: #00000099; --color-icon: #00000099;
--color-icon-white: #000000; --color-icon-white: #000000;
--color-border: #00000028; --color-border: #00000028;
--color-border-soft: #00000028;
--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.15);
--color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3); --color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.3);
--navbar-background: rgba(255, 255, 255, 0.8); --navbar-background-mac: rgba(255, 255, 255, 0.75);
--sidebar-background: rgba(255, 255, 255, 0.8); --navbar-background: rgba(255, 255, 255);
--input-bar-background: rgba(0, 0, 0, 0.02); --input-bar-background: rgba(0, 0, 0, 0.02);
} }
@@ -99,6 +102,14 @@ body[theme-mode='light'] {
font-weight: normal; font-weight: normal;
} }
*:focus {
outline: none;
}
* {
-webkit-tap-highlight-color: transparent;
}
ul { ul {
list-style: none; list-style: none;
} }
@@ -162,3 +173,34 @@ body,
.ant-segmented-group { .ant-segmented-group {
gap: 4px; gap: 4px;
} }
.drag {
-webkit-app-region: drag;
}
.nodrag {
-webkit-app-region: no-drag;
}
.minapp-drawer {
.ant-drawer-content-wrapper {
box-shadow: none;
}
.ant-drawer-header {
position: absolute;
-webkit-app-region: drag;
min-height: calc(var(--navbar-height) + 0.5px);
background: var(--navbar-background);
width: calc(100vw - var(--sidebar-width));
border-bottom: 0.5px solid var(--color-border);
margin-top: -0.5px;
}
.ant-drawer-body {
padding: 0;
margin-top: var(--navbar-height);
overflow: hidden;
}
.minapp-mask {
background-color: transparent !important;
}
}

View File

@@ -0,0 +1,7 @@
import { FC } from 'react'
const CopyIcon: FC<React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>> = (props) => {
return <i {...props} className={`iconfont icon-copy ${props.className}`} />
}
export default CopyIcon

View File

@@ -0,0 +1,162 @@
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
import { isMac, isWindows } from '@renderer/config/constant'
import { useBridge } from '@renderer/hooks/useBridge'
import store from '@renderer/store'
import { setMinappShow } from '@renderer/store/runtime'
import { MinAppType } from '@renderer/types'
import { Drawer } from 'antd'
import { useRef, useState } from 'react'
import styled from 'styled-components'
import { TopView } from '../TopView'
interface Props {
app: MinAppType
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
const [open, setOpen] = useState(true)
const iframeRef = useRef<HTMLIFrameElement>(null)
useBridge()
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
const onClose = () => {
setOpen(false)
setTimeout(() => resolve({}), 300)
}
const onReload = () => {
if (iframeRef.current) {
iframeRef.current.src = app.url
}
}
const onOpenLink = () => {
window.api.openWebsite(app.url)
}
const Title = () => {
return (
<TitleContainer style={{ justifyContent: 'space-between' }}>
<TitleText>{app.name}</TitleText>
<ButtonsGroup className={isWindows ? 'windows' : ''}>
<Button onClick={onReload}>
<ReloadOutlined />
</Button>
{canOpenExternalLink && (
<Button onClick={onOpenLink}>
<ExportOutlined />
</Button>
)}
<Button onClick={onClose}>
<CloseOutlined />
</Button>
</ButtonsGroup>
</TitleContainer>
)
}
return (
<Drawer
title={<Title />}
placement="bottom"
onClose={onClose}
open={open}
mask={true}
rootClassName="minapp-drawer"
maskClassName="minapp-mask"
height={'100%'}
maskClosable={false}
closeIcon={null}
style={{ marginLeft: 'var(--sidebar-width)' }}>
<Frame src={app.url} ref={iframeRef} />
</Drawer>
)
}
const Frame = styled.iframe`
width: calc(100vw - var(--sidebar-width));
height: calc(100vh - var(--navbar-height));
border: none;
background-color: white;
`
const TitleContainer = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding-left: ${isMac ? '20px' : '15px'};
padding-right: 10px;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
`
const TitleText = styled.div`
font-weight: bold;
font-size: 14px;
color: var(--color-text-1);
margin-right: 10px;
user-select: none;
`
const ButtonsGroup = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
-webkit-app-region: no-drag;
&.windows {
margin-right: ${isWindows ? '130px' : 0};
background-color: var(--color-background-mute);
border-radius: 50px;
padding: 0 3px;
overflow: hidden;
}
`
const Button = styled.div`
cursor: pointer;
width: 30px;
height: 30px;
border-radius: 5px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
color: var(--color-text-2);
transition: all 0.2s ease;
font-size: 14px;
&:hover {
color: var(--color-text-1);
background-color: var(--color-background-mute);
}
`
export default class MinApp {
static topviewId = 0
static close() {
TopView.hide('MinApp')
store.dispatch(setMinappShow(false))
}
static start(app: MinAppType) {
store.dispatch(setMinappShow(true))
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
app={app}
resolve={(v) => {
resolve(v)
this.close()
}}
/>,
'MinApp'
)
})
}
}

View File

@@ -0,0 +1,108 @@
import useAvatar from '@renderer/hooks/useAvatar'
import { useSettings } from '@renderer/hooks/useSettings'
import LocalStorage from '@renderer/services/storage'
import { useAppDispatch } from '@renderer/store'
import { setAvatar } from '@renderer/store/runtime'
import { setUserName } from '@renderer/store/settings'
import { compressImage } from '@renderer/utils'
import { Avatar, Input, Modal, Upload } from 'antd'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { Center, HStack } from '../Layout'
import { TopView } from '../TopView'
interface Props {
resolve: (data: any) => void
}
const PopupContainer: React.FC<Props> = ({ resolve }) => {
const [open, setOpen] = useState(true)
const { t } = useTranslation()
const { userName } = useSettings()
const dispatch = useAppDispatch()
const avatar = useAvatar()
const onOk = () => {
setOpen(false)
}
const onCancel = () => {
setOpen(false)
}
const onClose = () => {
resolve({})
}
return (
<Modal
width="300px"
open={open}
footer={null}
onOk={onOk}
onCancel={onCancel}
afterClose={onClose}
transitionName="ant-move-down">
<Center mt="30px">
<Upload
customRequest={() => {}}
accept="image/png, image/jpeg"
itemRender={() => null}
maxCount={1}
onChange={async ({ file }) => {
try {
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} />
</Upload>
</Center>
<HStack alignItems="center" gap="10px" p="20px">
<Input
placeholder={t('settings.general.user_name.placeholder')}
value={userName}
onChange={(e) => dispatch(setUserName(e.target.value))}
style={{ flex: 1, textAlign: 'center', width: '100%' }}
maxLength={30}
/>
</HStack>
</Modal>
)
}
const UserAvatar = styled(Avatar)`
cursor: pointer;
width: 80px;
height: 80px;
transition: opacity 0.3s ease;
&:hover {
opacity: 0.8;
}
`
export default class UserPopup {
static topviewId = 0
static hide() {
TopView.hide('UserPopup')
}
static show() {
return new Promise<any>((resolve) => {
TopView.show(
<PopupContainer
resolve={(v) => {
resolve(v)
this.hide()
}}
/>,
'UserPopup'
)
})
}
}

View File

@@ -1,11 +1,21 @@
import { isMac } from '@renderer/config/constant' import { isMac } from '@renderer/config/constant'
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 }) => {
return <NavbarContainer {...props}>{children}</NavbarContainer> const { minappShow } = useRuntime()
const backgroundColor = minappShow ? 'var(--navbar-background)' : navbarBackgroundColor
return (
<NavbarContainer {...props} style={{ backgroundColor }}>
{children}
</NavbarContainer>
)
} }
export const NavbarLeft: FC<Props> = ({ children, ...props }) => { export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
@@ -26,15 +36,16 @@ const NavbarContainer = styled.div`
flex-direction: row; flex-direction: row;
min-height: var(--navbar-height); min-height: var(--navbar-height);
max-height: var(--navbar-height); max-height: var(--navbar-height);
-webkit-app-region: drag; margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
margin-left: calc(var(--sidebar-width) * -1);
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: var(--navbar-background); background-color: ${navbarBackgroundColor};
transition: background-color 0.3s ease;
-webkit-app-region: drag;
` `
const NavbarLeftContainer = styled.div` const NavbarLeftContainer = styled.div`
min-width: ${isMac ? 'var(--assistants-width)' : 'calc(var(--sidebar-width) + var(--assistants-width))'}; min-width: var(--assistants-width);
padding: 0 10px; padding: 0 10px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -56,5 +67,5 @@ const NavbarRightContainer = styled.div`
min-width: var(--settings-width); min-width: var(--settings-width);
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 16px; padding: 0 12px;
` `

View File

@@ -1,21 +1,31 @@
import { TranslationOutlined } from '@ant-design/icons' import { TranslationOutlined } from '@ant-design/icons'
import Logo from '@renderer/assets/images/logo.png' import Logo from '@renderer/assets/images/logo.png'
import { isMac } from '@renderer/config/constant'
import useAvatar from '@renderer/hooks/useAvatar' import useAvatar from '@renderer/hooks/useAvatar'
import { useRuntime } from '@renderer/hooks/useStore'
import { Avatar } from 'antd'
import { FC } from 'react' import { FC } from 'react'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'
import styled from 'styled-components' import styled from 'styled-components'
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 isRoute = (path: string): string => (pathname === path ? 'active' : '') const isRoute = (path: string): string => (pathname === path ? 'active' : '')
const onEditUser = () => {
UserPopup.show()
}
return ( return (
<Container> <Container style={{ backgroundColor: minappShow ? 'var(--navbar-background)' : sidebarBackgroundColor }}>
<StyledLink to="/"> <AvatarImg src={avatar || Logo} draggable={false} className="nodrag" onClick={onEditUser} />
<AvatarImg src={avatar || Logo} draggable={false} />
</StyledLink>
<MainMenus> <MainMenus>
<Menus> <Menus>
<StyledLink to="/"> <StyledLink to="/">
@@ -23,9 +33,9 @@ const Sidebar: FC = () => {
<i className="iconfont icon-chat"></i> <i className="iconfont icon-chat"></i>
</Icon> </Icon>
</StyledLink> </StyledLink>
<StyledLink to="/apps"> <StyledLink to="/agents">
<Icon className={isRoute('/apps')}> <Icon className={isRoute('/agents')}>
<i className="iconfont icon-appstore"></i> <i className="iconfont icon-business-smart-assistant"></i>
</Icon> </Icon>
</StyledLink> </StyledLink>
<StyledLink to="/translate"> <StyledLink to="/translate">
@@ -33,6 +43,11 @@ const Sidebar: FC = () => {
<TranslationOutlined /> <TranslationOutlined />
</Icon> </Icon>
</StyledLink> </StyledLink>
<StyledLink to="/apps">
<Icon className={isRoute('/apps')}>
<i className="iconfont icon-appstore"></i>
</Icon>
</StyledLink>
</Menus> </Menus>
</MainMenus> </MainMenus>
<Menus> <Menus>
@@ -52,21 +67,23 @@ const Container = styled.div`
align-items: center; align-items: center;
padding: 8px 0; padding: 8px 0;
width: var(--sidebar-width); width: var(--sidebar-width);
height: calc(100vh - var(--navbar-height)); min-width: var(--sidebar-width);
height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
-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: var(--navbar-height); margin-top: ${isMac ? 'var(--navbar-height)' : 0};
margin-bottom: var(--navbar-height); background-color: ${sidebarBackgroundColor};
background-color: var(--sidebar-background); transition: background-color 0.3s ease;
` `
const AvatarImg = styled.img` const AvatarImg = styled(Avatar)`
border-radius: 50%;
width: 28px; width: 28px;
height: 28px; height: 28px;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
margin: 5px 0; margin-bottom: ${isMac ? '12px' : '12px'};
margin-top: 5px; margin-top: ${isMac ? '5px' : '2px'};
border: none;
cursor: pointer;
` `
const MainMenus = styled.div` const MainMenus = styled.div`
display: flex; display: flex;

View File

@@ -353,6 +353,36 @@ export const SYSTEM_MODELS: Record<string, SystemModel[]> = {
} }
], ],
doubao: [], doubao: [],
minimax: [
{
id: 'abab6.5s-chat',
provider: 'minimax',
name: 'abab6.5s',
group: 'abab6',
enabled: true
},
{
id: 'abab6.5g-chat',
provider: 'minimax',
name: 'abab6.5g',
group: 'abab6',
enabled: true
},
{
id: 'abab6.5t-chat',
provider: 'minimax',
name: 'abab6.5t',
group: 'abab6',
enabled: true
},
{
id: 'abab5.5s-chat',
provider: 'minimax',
name: 'abab5.5s',
group: 'abab5',
enabled: true
}
],
aihubmix: [ aihubmix: [
{ {
id: 'gpt-4o-mini', id: 'gpt-4o-mini',

View File

@@ -1,3 +1,6 @@
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp'
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
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.jpeg'
import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg' import ChatGPTModelLogo from '@renderer/assets/images/models/chatgpt.jpeg'
@@ -7,13 +10,14 @@ 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'
import GeminiModelLogo from '@renderer/assets/images/models/gemini.png' import GeminiModelLogo from '@renderer/assets/images/models/gemini.png'
import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg' import GemmaModelLogo from '@renderer/assets/images/models/gemma.jpeg'
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 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'
import StepModelLogo from '@renderer/assets/images/models/step.jpg' import StepModelLogo from '@renderer/assets/images/models/step.jpg'
import YiModelLogo from '@renderer/assets/images/models/yi.svg' import YiModelLogo from '@renderer/assets/images/models/yi.png'
import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg' import AiHubMixProviderLogo from '@renderer/assets/images/providers/aihubmix.jpg'
import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg' import AnthropicProviderLogo from '@renderer/assets/images/providers/anthropic.jpeg'
import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png' import BaichuanProviderLogo from '@renderer/assets/images/providers/baichuan.png'
@@ -23,14 +27,15 @@ 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 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 MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpeg' import MinimaxProviderLogo from '@renderer/assets/images/providers/minimax.png'
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpeg' import MoonshotProviderLogo from '@renderer/assets/images/providers/moonshot.jpg'
import MoonshotModelLogo from '@renderer/assets/images/providers/moonshot.jpg'
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png' import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png' import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png' import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png' import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
import StepFunProviderLogo from '@renderer/assets/images/providers/stepfun.png' import StepFunProviderLogo from '@renderer/assets/images/providers/stepfun.png'
import YiProviderLogo from '@renderer/assets/images/providers/yi.svg' import YiProviderLogo from '@renderer/assets/images/providers/yi.png'
import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png' import ZhipuProviderLogo from '@renderer/assets/images/providers/zhipu.png'
export function getProviderLogo(providerId: string) { export function getProviderLogo(providerId: string) {
@@ -69,6 +74,8 @@ export function getProviderLogo(providerId: string) {
return DoubaoProviderLogo return DoubaoProviderLogo
case 'graphrag-kylin-mountain': case 'graphrag-kylin-mountain':
return GraphRagProviderLogo return GraphRagProviderLogo
case 'minimax':
return MinimaxProviderLogo
default: default:
return undefined return undefined
} }
@@ -98,6 +105,7 @@ export function getModelLogo(modelId: string) {
bison: PalmModelLogo, bison: PalmModelLogo,
palm: PalmModelLogo, palm: PalmModelLogo,
step: StepModelLogo, step: StepModelLogo,
abab: HailuoModelLogo,
'ep-202': DoubaoModelLogo 'ep-202': DoubaoModelLogo
} }
@@ -121,6 +129,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://platform.openai.com/api-keys', apiKey: 'https://platform.openai.com/api-keys',
docs: 'https://platform.openai.com/docs', docs: 'https://platform.openai.com/docs',
models: 'https://platform.openai.com/docs/models' models: 'https://platform.openai.com/docs/models'
},
app: {
name: 'ChatGPT',
url: 'https://chatgpt.com/',
logo: OpenAiProviderLogo
} }
}, },
gemini: { gemini: {
@@ -133,6 +146,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://aistudio.google.com/app/apikey', apiKey: 'https://aistudio.google.com/app/apikey',
docs: 'https://ai.google.dev/gemini-api/docs', docs: 'https://ai.google.dev/gemini-api/docs',
models: 'https://ai.google.dev/gemini-api/docs/models/gemini' models: 'https://ai.google.dev/gemini-api/docs/models/gemini'
},
app: {
name: 'Gemini',
url: 'https://gemini.google.com/',
logo: GeminiProviderLogo
} }
}, },
silicon: { silicon: {
@@ -145,6 +163,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://cloud.siliconflow.cn/account/ak?referrer=clxty1xuy0014lvqwh5z50i88', apiKey: 'https://cloud.siliconflow.cn/account/ak?referrer=clxty1xuy0014lvqwh5z50i88',
docs: 'https://docs.siliconflow.cn/', docs: 'https://docs.siliconflow.cn/',
models: 'https://docs.siliconflow.cn/docs/model-names' models: 'https://docs.siliconflow.cn/docs/model-names'
},
app: {
name: 'SiliconFlow',
url: 'https://cloud.siliconflow.cn/playground/chat',
logo: SiliconFlowProviderLogo
} }
}, },
deepseek: { deepseek: {
@@ -157,6 +180,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://platform.deepseek.com/api_keys', apiKey: 'https://platform.deepseek.com/api_keys',
docs: 'https://platform.deepseek.com/api-docs/', docs: 'https://platform.deepseek.com/api-docs/',
models: 'https://platform.deepseek.com/api-docs/' models: 'https://platform.deepseek.com/api-docs/'
},
app: {
name: 'DeepSeek',
url: 'https://chat.deepseek.com/',
logo: DeepSeekProviderLogo
} }
}, },
yi: { yi: {
@@ -169,6 +197,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://platform.lingyiwanwu.com/apikeys', apiKey: 'https://platform.lingyiwanwu.com/apikeys',
docs: 'https://platform.lingyiwanwu.com/docs', docs: 'https://platform.lingyiwanwu.com/docs',
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: {
name: 'Yi',
url: 'https://www.wanzhi.com/',
logo: YiProviderLogo
} }
}, },
zhipu: { zhipu: {
@@ -181,6 +214,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://open.bigmodel.cn/usercenter/apikeys', apiKey: 'https://open.bigmodel.cn/usercenter/apikeys',
docs: 'https://open.bigmodel.cn/dev/howuse/introduction', docs: 'https://open.bigmodel.cn/dev/howuse/introduction',
models: 'https://open.bigmodel.cn/modelcenter/square' models: 'https://open.bigmodel.cn/modelcenter/square'
},
app: {
name: '智谱',
url: 'https://chatglm.cn/main/alltoolsdetail',
logo: ZhipuProviderLogo
} }
}, },
moonshot: { moonshot: {
@@ -193,6 +231,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://platform.moonshot.cn/console/api-keys', apiKey: 'https://platform.moonshot.cn/console/api-keys',
docs: 'https://platform.moonshot.cn/docs/', docs: 'https://platform.moonshot.cn/docs/',
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: {
name: 'Kimi',
url: 'https://kimi.moonshot.cn/',
logo: KimiAppLogo
} }
}, },
baichuan: { baichuan: {
@@ -205,6 +248,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://platform.baichuan-ai.com/console/apikey', apiKey: 'https://platform.baichuan-ai.com/console/apikey',
docs: 'https://platform.baichuan-ai.com/docs', docs: 'https://platform.baichuan-ai.com/docs',
models: 'https://platform.baichuan-ai.com/price' models: 'https://platform.baichuan-ai.com/price'
},
app: {
name: '百小应',
url: 'https://ying.baichuan-ai.com/chat',
logo: BaicuanAppLogo
} }
}, },
dashscope: { dashscope: {
@@ -217,6 +265,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://help.aliyun.com/zh/dashscope/developer-reference/acquisition-and-configuration-of-api-key', apiKey: 'https://help.aliyun.com/zh/dashscope/developer-reference/acquisition-and-configuration-of-api-key',
docs: 'https://help.aliyun.com/zh/dashscope/', docs: 'https://help.aliyun.com/zh/dashscope/',
models: 'https://dashscope.console.aliyun.com/model' models: 'https://dashscope.console.aliyun.com/model'
},
app: {
name: '通义千问',
url: 'https://tongyi.aliyun.com/qianwen/',
logo: QwenModelLogo
} }
}, },
stepfun: { stepfun: {
@@ -229,6 +282,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://platform.stepfun.com/interface-key', apiKey: 'https://platform.stepfun.com/interface-key',
docs: 'https://platform.stepfun.com/docs/overview/concept', docs: 'https://platform.stepfun.com/docs/overview/concept',
models: 'https://platform.stepfun.com/docs/llm/text' models: 'https://platform.stepfun.com/docs/llm/text'
},
app: {
name: '跃问',
url: 'https://yuewen.cn/chats/new',
logo: YuewenAppLogo
} }
}, },
doubao: { doubao: {
@@ -241,6 +299,28 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey', apiKey: 'https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey',
docs: 'https://www.volcengine.com/docs/82379/1182403', docs: 'https://www.volcengine.com/docs/82379/1182403',
models: 'https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint' models: 'https://console.volcengine.com/ark/region:ark+cn-beijing/endpoint'
},
app: {
name: '豆包',
url: 'https://www.doubao.com/chat/',
logo: DoubaoProviderLogo
}
},
minimax: {
api: {
url: 'https://api.minimax.chat/v1/',
editable: true
},
websites: {
official: 'https://platform.minimaxi.com/',
apiKey: 'https://platform.minimaxi.com/user-center/basic-information/interface-key',
docs: 'https://platform.minimaxi.com/document/Announcement',
models: 'https://platform.minimaxi.com/document/Models'
},
app: {
name: '海螺',
url: 'https://hailuoai.com/',
logo: HailuoModelLogo
} }
}, },
'graphrag-kylin-mountain': { 'graphrag-kylin-mountain': {
@@ -271,6 +351,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://console.groq.com/keys', apiKey: 'https://console.groq.com/keys',
docs: 'https://console.groq.com/docs/quickstart', docs: 'https://console.groq.com/docs/quickstart',
models: 'https://console.groq.com/docs/models' models: 'https://console.groq.com/docs/models'
},
app: {
name: 'Groq',
url: 'https://chat.groq.com/',
logo: GroqProviderLogo
} }
}, },
ollama: { ollama: {
@@ -294,6 +379,11 @@ export const PROVIDER_CONFIG = {
apiKey: 'https://console.anthropic.com/settings/keys', apiKey: 'https://console.anthropic.com/settings/keys',
docs: 'https://docs.anthropic.com/en/docs', docs: 'https://docs.anthropic.com/en/docs',
models: 'https://docs.anthropic.com/en/docs/about-claude/models' models: 'https://docs.anthropic.com/en/docs/about-claude/models'
},
app: {
name: 'Claude',
url: 'https://claude.ai/',
logo: AnthropicProviderLogo
} }
}, },
aihubmix: { aihubmix: {

View File

@@ -0,0 +1,51 @@
import { useEffect } from 'react'
export function useBridge() {
useEffect(() => {
const handleMessage = async (event: MessageEvent) => {
const targetOrigin = { targetOrigin: '*' }
try {
if (event.origin !== 'file://') {
return
}
const { type, method, args, id } = event.data
if (type !== 'api-call' || !window.api) {
return
}
const apiMethod = window.api[method]
if (typeof apiMethod !== 'function') {
return
}
event.source?.postMessage(
{
id,
type: 'api-response',
result: await apiMethod(...args)
},
targetOrigin
)
} catch (error) {
event.source?.postMessage(
{
id: event.data?.id,
type: 'api-response',
error: error instanceof Error ? error.message : String(error)
},
targetOrigin
)
}
}
window.addEventListener('message', handleMessage)
return () => {
window.removeEventListener('message', handleMessage)
}
}, [])
}

View File

@@ -28,7 +28,8 @@ const resources = {
footnotes: 'References', footnotes: 'References',
select: 'Select', select: 'Select',
search: 'Search', search: 'Search',
default: 'Default' default: 'Default',
warning: 'Warning'
}, },
button: { button: {
add: 'Add', add: 'Add',
@@ -48,7 +49,11 @@ const resources = {
'api.connection.failed': 'Connection failed', 'api.connection.failed': 'Connection failed',
'api.connection.success': 'Connection successful', 'api.connection.success': 'Connection successful',
'chat.completion.paused': 'Chat completion paused', 'chat.completion.paused': 'Chat completion paused',
'switch.disabled': 'Switching is disabled while the assistant is generating' 'switch.disabled': 'Switching is disabled while the assistant is generating',
'restore.success': 'Restored successfully',
'reset.confirm.content': 'Are you sure you want to clear all data?',
'reset.double.confirm.title': 'DATA LOST !!!',
'reset.double.confirm.content': 'All data will be lost, do you want to continue?'
}, },
chat: { chat: {
save: 'Save', save: 'Save',
@@ -121,6 +126,7 @@ const resources = {
aihubmix: 'AiHubMix', aihubmix: 'AiHubMix',
stepfun: 'StepFun', stepfun: 'StepFun',
doubao: 'Doubao', doubao: 'Doubao',
minimax: 'MiniMax',
'graphrag-kylin-mountain': 'GraphRAG' 'graphrag-kylin-mountain': 'GraphRAG'
}, },
settings: { settings: {
@@ -140,6 +146,9 @@ const resources = {
'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.reset.title': 'Data Reset',
'general.reset.button': 'Reset',
'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',
@@ -218,8 +227,16 @@ const resources = {
'keep_alive_time.placeholder': 'Minutes', 'keep_alive_time.placeholder': 'Minutes',
'keep_alive_time.description': 'The time in minutes to keep the connection alive, default is 5 minutes.' 'keep_alive_time.description': 'The time in minutes to keep the connection alive, default is 5 minutes.'
}, },
minapp: {
title: 'MinApp'
},
error: { error: {
'chat.response': 'Something went wrong. Please check if you have set your API key in the Settings > Providers' 'chat.response': 'Something went wrong. Please check if you have set your API key in the Settings > Providers',
'backup.file_format': 'Backup file format error'
},
words: {
knowledgeGraph: 'Knowledge Graph',
visualization: 'Visualization'
} }
} }
}, },
@@ -248,7 +265,8 @@ const resources = {
footnote: '引用内容', footnote: '引用内容',
select: '选择', select: '选择',
search: '搜索', search: '搜索',
default: '默认' default: '默认',
warning: '警告'
}, },
button: { button: {
add: '添加', add: '添加',
@@ -268,7 +286,11 @@ const resources = {
'api.connection.failed': '连接失败', 'api.connection.failed': '连接失败',
'api.connection.success': '连接成功', 'api.connection.success': '连接成功',
'chat.completion.paused': '会话已停止', 'chat.completion.paused': '会话已停止',
'switch.disabled': '模型回复完成后才能切换' 'switch.disabled': '模型回复完成后才能切换',
'restore.success': '恢复成功',
'reset.confirm.content': '确定要重置所有数据吗?',
'reset.double.confirm.title': '数据丢失!!!',
'reset.double.confirm.content': '你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?'
}, },
chat: { chat: {
save: '保存', save: '保存',
@@ -342,6 +364,7 @@ const resources = {
aihubmix: 'AiHubMix', aihubmix: 'AiHubMix',
stepfun: '阶跃星辰', stepfun: '阶跃星辰',
doubao: '豆包', doubao: '豆包',
minimax: 'MiniMax',
'graphrag-kylin-mountain': 'GraphRAG' 'graphrag-kylin-mountain': 'GraphRAG'
}, },
settings: { settings: {
@@ -361,6 +384,9 @@ const resources = {
'general.title': '常规设置', 'general.title': '常规设置',
'general.user_name': '用户名', 'general.user_name': '用户名',
'general.user_name.placeholder': '请输入用户名', 'general.user_name.placeholder': '请输入用户名',
'general.backup.title': '数据备份与恢复',
'general.reset.title': '重置数据',
'general.reset.button': '重置',
'provider.api_key': 'API 密钥', 'provider.api_key': 'API 密钥',
'provider.check': '检查', 'provider.check': '检查',
'provider.get_api_key': '点击这里获取密钥', 'provider.get_api_key': '点击这里获取密钥',
@@ -439,8 +465,16 @@ const resources = {
'keep_alive_time.placeholder': '分钟', 'keep_alive_time.placeholder': '分钟',
'keep_alive_time.description': '对话后模型在内存中保持的时间默认5分钟' 'keep_alive_time.description': '对话后模型在内存中保持的时间默认5分钟'
}, },
minapp: {
title: '小程序'
},
error: { error: {
'chat.response': '出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥' 'chat.response': '出错了,如果没有配置 API 密钥,请前往设置 > 模型提供商中配置密钥',
'backup.file_format': '备份文件格式错误'
},
words: {
knowledgeGraph: '知识图谱',
visualization: '可视化'
} }
} }
} }

View File

@@ -1,6 +1,5 @@
import './assets/styles/index.scss' import './assets/styles/index.scss'
import './init' import './init'
import './i18n'
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'

View File

@@ -56,13 +56,13 @@ const AppsPage: FC = () => {
<ContentContainer> <ContentContainer>
<AssistantsContainer> <AssistantsContainer>
<HStack alignItems="center" style={{ marginBottom: 16 }}> <HStack alignItems="center" style={{ marginBottom: 16 }}>
<Title level={3}>{t('agents.my_agents')}</Title> <Title level={4}>{t('agents.my_agents')}</Title>
{agents.length > 0 && <ManageIcon onClick={ManageAgentsPopup.show} />} {agents.length > 0 && <ManageIcon onClick={ManageAgentsPopup.show} />}
</HStack> </HStack>
<UserAgents onAdd={onAddAgentConfirm} /> <UserAgents onAdd={onAddAgentConfirm} />
{Object.keys(agentGroups).map((group) => ( {Object.keys(agentGroups).map((group) => (
<div key={group}> <div key={group}>
<Title level={3} key={group} style={{ marginBottom: 16 }}> <Title level={4} key={group} style={{ marginBottom: 16 }}>
{group} {group}
</Title> </Title>
<Row gutter={16}> <Row gutter={16}>

View File

@@ -1,5 +1,5 @@
import { Agent } from '@renderer/types' import { Agent } from '@renderer/types'
import { Col, Typography } from 'antd' import { Col } from 'antd'
import styled from 'styled-components' import styled from 'styled-components'
interface Props { interface Props {
@@ -7,17 +7,13 @@ interface Props {
onClick?: () => void onClick?: () => void
} }
const { Title } = Typography
const AgentCard: React.FC<Props> = ({ agent, onClick }) => { const AgentCard: React.FC<Props> = ({ agent, onClick }) => {
return ( return (
<Container onClick={onClick}> <Container onClick={onClick}>
{agent.emoji && <EmojiHeader>{agent.emoji}</EmojiHeader>} {agent.emoji && <EmojiHeader>{agent.emoji}</EmojiHeader>}
<Col> <Col>
<AgentHeader> <AgentHeader>
<AgentName level={5} style={{ marginBottom: 0 }}> <AgentName style={{ marginBottom: 0 }}>{agent.name}</AgentName>
{agent.name}
</AgentName>
</AgentHeader> </AgentHeader>
<AgentCardPrompt>{agent.prompt}</AgentCardPrompt> <AgentCardPrompt>{agent.prompt}</AgentCardPrompt>
</Col> </Col>
@@ -41,14 +37,14 @@ const Container = styled.div`
} }
` `
const EmojiHeader = styled.div` const EmojiHeader = styled.div`
width: 25px; width: 20px;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
margin-right: 5px; margin-right: 5px;
font-size: 25px; font-size: 20px;
line-height: 25px; line-height: 20px;
` `
const AgentHeader = styled.div` const AgentHeader = styled.div`
@@ -58,15 +54,13 @@ const AgentHeader = styled.div`
align-items: center; align-items: center;
` `
const AgentName = styled(Title)` const AgentName = styled.div`
font-size: 18px;
line-height: 1.2; line-height: 1.2;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
color: var(--color-white); color: var(--color-text-1);
font-weight: 900;
` `
const AgentCardPrompt = styled.div` const AgentCardPrompt = styled.div`
@@ -76,6 +70,7 @@ const AgentCardPrompt = styled.div`
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
overflow: hidden; overflow: hidden;
font-size: 12px;
` `
export default AgentCard export default AgentCard

View File

@@ -41,10 +41,10 @@ const AssistantCardContainer = styled.div`
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 20px; padding: 20px;
border: 1px dashed var(--color-border); border: 1px dashed var(--color-border-soft);
border-radius: 10px; border-radius: 10px;
cursor: pointer; cursor: pointer;
min-height: 84px; min-height: 72px;
.anticon { .anticon {
font-size: 16px; font-size: 16px;
color: var(--color-icon); color: var(--color-icon);

View File

@@ -0,0 +1,58 @@
import MinApp from '@renderer/components/MinApp'
import { useTheme } from '@renderer/providers/ThemeProvider'
import { MinAppType } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
interface Props {
app: MinAppType
}
const App: FC<Props> = ({ app }) => {
const { theme } = useTheme()
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)
}
return (
<Container onClick={onClick}>
<AppIcon src={app.logo} style={{ border: theme === 'dark' ? 'none' : '0.5px solid var(--color-border' }} />
<AppTitle>{app.name}</AppTitle>
</Container>
)
}
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
width: 65px;
`
const AppIcon = styled.img`
width: 60px;
height: 60px;
border-radius: 16px;
user-select: none;
-webkit-user-drag: none;
`
const AppTitle = styled.div`
font-size: 12px;
margin-top: 5px;
color: var(--color-text-soft);
text-align: center;
user-select: none;
`
export default App

View File

@@ -0,0 +1,148 @@
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 { Center } from '@renderer/components/Layout'
import { PROVIDER_CONFIG } from '@renderer/config/provider'
import { MinAppType } from '@renderer/types'
import { Empty, Input } from 'antd'
import { isEmpty } from 'lodash'
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
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/'
}
]
const AppsPage: FC = () => {
const { t } = useTranslation()
const [search, setSearch] = useState('')
const list: MinAppType[] = (Object.entries(PROVIDER_CONFIG) as any[])
.filter(([, config]) => config.app)
.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())
)
: list
return (
<Container>
<Navbar>
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
{t('minapp.title')}
<Input
placeholder={t('common.search')}
className="nodrag"
style={{ width: '30%', height: 28 }}
size="small"
variant="filled"
suffix={<SearchOutlined />}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div style={{ width: 80 }} />
</NavbarCenter>
</Navbar>
<ContentContainer>
<AppsContainer>
{apps.map((app) => (
<App key={app.name} app={app} />
))}
{isEmpty(apps) && (
<Center style={{ flex: 1 }}>
<Empty />
</Center>
)}
</AppsContainer>
</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: 50px;
`
const AppsContainer = styled.div`
display: flex;
min-width: 900px;
max-width: 900px;
flex-direction: row;
flex-wrap: wrap;
align-content: flex-start;
gap: 50px;
`
export default AppsPage

View File

@@ -145,7 +145,7 @@ const AssistantItem = styled.div`
flex-direction: column; flex-direction: column;
padding: 7px 10px; padding: 7px 10px;
position: relative; position: relative;
border-radius: 8px; border-radius: 4px;
cursor: pointer; cursor: pointer;
font-family: Ubuntu; font-family: Ubuntu;
.anticon { .anticon {

View File

@@ -6,7 +6,7 @@ import { FC } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Inputbar from './Inputbar/Inputbar' import Inputbar from './Inputbar/Inputbar'
import Messages from './Messages' import Messages from './Messages/Messages'
import RightSidebar from './RightSidebar' import RightSidebar from './RightSidebar'
interface Props { interface Props {

View File

@@ -9,9 +9,9 @@ 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 Assistants from './Assistants'
import Chat from './Chat' import Chat from './Chat'
import AddAssistantPopup from './components/AddAssistantPopup'
import Navigation from './Header' import Navigation from './Header'
let _activeAssistant: Assistant let _activeAssistant: Assistant
@@ -51,7 +51,7 @@ const HomePage: FC = () => {
</NavbarLeft> </NavbarLeft>
)} )}
<Navigation activeAssistant={activeAssistant} /> <Navigation activeAssistant={activeAssistant} />
<NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 8 }}> <NavbarRight style={{ justifyContent: 'flex-end', paddingRight: isWindows ? 140 : 12 }}>
<ThemeSwitch <ThemeSwitch
checkedChildren={<i className="iconfont icon-theme icon-dark1" />} checkedChildren={<i className="iconfont icon-theme icon-dark1" />}
unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />} unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />}
@@ -120,7 +120,7 @@ export const NewButton = styled.div`
const ThemeSwitch = styled(Switch)` const ThemeSwitch = styled(Switch)`
-webkit-app-region: none; -webkit-app-region: none;
margin-right: 8px; margin-right: 10px;
.icon-theme { .icon-theme {
font-size: 14px; font-size: 14px;
} }

View File

@@ -70,6 +70,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message) EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
setText('') setText('')
setTimeout(() => setText(''), 500)
setExpend(false) setExpend(false)
}, [assistant.id, assistant.topics, generating, text]) }, [assistant.id, assistant.topics, generating, text])
@@ -193,15 +194,17 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
<Tag <Tag
style={{ style={{
cursor: 'pointer', cursor: 'pointer',
borderRadius: '20px', borderRadius: '6px',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
padding: '2px 8px' padding: '2px 8px',
borderWidth: 0.5
}}> }}>
<i className="iconfont icon-history" style={{ marginRight: '3px' }} /> <i className="iconfont icon-history" style={{ marginRight: '3px' }} />
{assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT} {assistant?.settings?.contextCount ?? DEFAULT_CONEXTCOUNT}
<Divider type="vertical" style={{ marginTop: 2, marginLeft: 5, marginRight: 5 }} /> <Divider type="vertical" style={{ marginTop: 2, marginLeft: 5, marginRight: 5 }} />{inputTokenCount}
{`${inputTokenCount} / ${estimateTokenCount}`} <span style={{ margin: '0 2px' }}>/</span>
{estimateTokenCount}
</Tag> </Tag>
</Tooltip> </Tooltip>
</TextCount> </TextCount>
@@ -240,7 +243,7 @@ const Container = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: var(--input-bar-height); height: var(--input-bar-height);
border: 1px solid var(--color-border); border: 1px solid var(--color-border-soft);
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative; position: relative;
margin: 0 20px 15px 20px; margin: 0 20px 15px 20px;
@@ -303,7 +306,6 @@ const TextCount = styled.div`
padding: 2px; padding: 2px;
border-top-left-radius: 7px; border-top-left-radius: 7px;
user-select: none; user-select: none;
margin-right: 10px;
` `
export default Inputbar export default Inputbar

View File

@@ -1,4 +1,5 @@
import { CheckOutlined, CopyOutlined } from '@ant-design/icons' import { CheckOutlined } from '@ant-design/icons'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { initMermaid } from '@renderer/init' import { initMermaid } from '@renderer/init'
import { useTheme } from '@renderer/providers/ThemeProvider' import { useTheme } from '@renderer/providers/ThemeProvider'
import { ThemeMode } from '@renderer/store/settings' import { ThemeMode } from '@renderer/store/settings'
@@ -39,7 +40,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className, ...rest }) =
<div> <div>
<CodeHeader> <CodeHeader>
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage> <CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
{!copied && <CopyOutlined className="copy" onClick={onCopy} />} {!copied && <CopyIcon className="copy" onClick={onCopy} />}
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />} {copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</CodeHeader> </CodeHeader>
<SyntaxHighlighter <SyntaxHighlighter

View File

@@ -23,8 +23,8 @@ import { FC, memo, useCallback, 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 SelectModelDropdown from './components/SelectModelDropdown' import SelectModelDropdown from '../components/SelectModelDropdown'
import Markdown from './Markdown/Markdown' import Markdown from '../Markdown/Markdown'
interface Props { interface Props {
message: Message message: Message
@@ -208,7 +208,7 @@ const MessageContainer = styled.div`
&.user { &.user {
position: absolute; position: absolute;
top: 10px; top: 10px;
right: 10px; right: 15px;
} }
} }
&:hover { &:hover {

View File

@@ -10,11 +10,12 @@ import { getBriefInfo, runAsyncFunction, uuid } from '@renderer/utils'
import { t } from 'i18next' import { t } from 'i18next'
import localforage from 'localforage' import localforage from 'localforage'
import { last, reverse } from 'lodash' import { last, reverse } from 'lodash'
import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { FC, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components' import styled from 'styled-components'
import Suggestions from './components/Suggestions' import Suggestions from '../components/Suggestions'
import MessageItem from './Message' import MessageItem from './Message'
import Prompt from './Prompt'
interface Props { interface Props {
assistant: Assistant assistant: Assistant
@@ -28,19 +29,6 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
const containerRef = useRef<HTMLDivElement>(null) const containerRef = useRef<HTMLDivElement>(null)
const { updateTopic } = useAssistant(assistant.id) const { updateTopic } = useAssistant(assistant.id)
const assistantDefaultMessage: Message = useMemo(
() => ({
id: 'assistant',
role: 'assistant',
content: assistant.description || assistant.prompt || t('chat.default.description'),
assistantId: assistant.id,
topicId: topic.id,
status: 'pending',
createdAt: new Date().toISOString()
}),
[assistant.description, assistant.id, assistant.prompt, topic.id]
)
const onSendMessage = useCallback( const onSendMessage = useCallback(
(message: Message) => { (message: Message) => {
const _messages = [...messages, message] const _messages = [...messages, message]
@@ -123,7 +111,7 @@ const Messages: FC<Props> = ({ assistant, topic }) => {
{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} />
))} ))}
<MessageItem message={assistantDefaultMessage} /> <Prompt assistant={assistant} key={assistant.prompt} />
</Container> </Container>
) )
} }

View File

@@ -0,0 +1,54 @@
import AssistantSettingPopup from '@renderer/components/Popups/AssistantSettingPopup'
import { useAssistant } from '@renderer/hooks/useAssistant'
import { syncAsistantToAgent } from '@renderer/services/assistant'
import { Assistant } from '@renderer/types'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
assistant: Assistant
}
const Prompt: FC<Props> = ({ assistant }) => {
const { t } = useTranslation()
const { updateAssistant } = useAssistant(assistant.id)
const prompt = assistant.prompt || t('chat.default.description')
const onEdit = async () => {
const _assistant = await AssistantSettingPopup.show({ assistant })
updateAssistant(_assistant)
syncAsistantToAgent(_assistant)
}
if (!prompt) {
return null
}
return (
<Container onClick={onEdit}>
<Text>{prompt}</Text>
</Container>
)
}
const Container = styled.div`
padding: 10px 20px;
background-color: var(--color-background-soft);
margin-bottom: 20px;
margin: 0 20px 20px 20px;
border-radius: 6px;
cursor: pointer;
`
const Text = styled.div`
color: var(--color-text-2);
font-size: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
`
export default Prompt

View File

@@ -142,12 +142,11 @@ const Container = styled.div`
const TopicListItem = styled.div` const TopicListItem = styled.div`
padding: 7px 10px; padding: 7px 10px;
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 4px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
font-family: Ubuntu; font-family: Ubuntu;
transition: all 0.3s;
&:hover { &:hover {
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
} }

View File

@@ -2,7 +2,7 @@ import { getModelLogo } from '@renderer/config/provider'
import { useProviders } from '@renderer/hooks/useProvider' import { useProviders } from '@renderer/hooks/useProvider'
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, sortBy, upperFirst } from 'lodash' import { first, reverse, sortBy, upperFirst } from 'lodash'
import { FC, PropsWithChildren } from 'react' import { FC, PropsWithChildren } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
@@ -22,7 +22,7 @@ const SelectModelDropdown: FC<Props & PropsWithChildren> = ({ children, model, o
key: p.id, key: p.id,
label: p.isSystem ? t(`provider.${p.id}`) : p.name, label: p.isSystem ? t(`provider.${p.id}`) : p.name,
type: 'group', type: 'group',
children: sortBy(p.models, 'name').map((m) => ({ children: reverse(sortBy(p.models, 'name')).map((m) => ({
key: m?.id, key: m?.id,
label: upperFirst(m?.name), label: upperFirst(m?.name),
defaultSelectedKeys: [model?.id], defaultSelectedKeys: [model?.id],

View File

@@ -2,6 +2,7 @@ import { GithubOutlined } from '@ant-design/icons'
import { FileProtectOutlined, GlobalOutlined, MailOutlined, SoundOutlined } from '@ant-design/icons' import { FileProtectOutlined, GlobalOutlined, MailOutlined, SoundOutlined } from '@ant-design/icons'
import Logo from '@renderer/assets/images/logo.png' import Logo from '@renderer/assets/images/logo.png'
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import MinApp from '@renderer/components/MinApp'
import { runAsyncFunction } from '@renderer/utils' import { runAsyncFunction } from '@renderer/utils'
import { Avatar, Button, Progress, Row, Tag } from 'antd' import { Avatar, Button, Progress, Row, Tag } from 'antd'
import { ProgressInfo } from 'electron-updater' import { ProgressInfo } from 'electron-updater'
@@ -139,7 +140,14 @@ const AboutSettings: FC = () => {
<SoundOutlined /> <SoundOutlined />
{t('settings.about.releases.title')} {t('settings.about.releases.title')}
</SettingRowTitle> </SettingRowTitle>
<Button onClick={() => onOpenWebsite('https://github.com/kangfenmao/cherry-studio/releases')}> <Button
onClick={() =>
MinApp.start({
name: t('settings.about.releases.title'),
url: 'https://github.com/kangfenmao/cherry-studio/releases',
logo: ''
})
}>
{t('settings.about.releases.button')} {t('settings.about.releases.button')}
</Button> </Button>
</SettingRow> </SettingRow>

View File

@@ -1,13 +1,16 @@
import { FolderOpenOutlined, SaveOutlined } from '@ant-design/icons'
import { HStack } from '@renderer/components/Layout'
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 i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import { backup, reset, restore } from '@renderer/services/backup'
import LocalStorage from '@renderer/services/storage' 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 { setFontSize, setLanguage, setUserName, ThemeMode } 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 { compressImage, isValidProxyUrl } from '@renderer/utils'
import { Avatar, Input, Select, Slider, Upload } from 'antd' import { Avatar, Button, Input, Select, Upload } 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 styled from 'styled-components'
@@ -16,8 +19,7 @@ import { SettingContainer, SettingDivider, SettingRow, SettingRowTitle, SettingT
const GeneralSettings: FC = () => { const GeneralSettings: FC = () => {
const avatar = useAvatar() const avatar = useAvatar()
const { language, proxyUrl: storeProxyUrl, userName, theme, setTheme, fontSize } = useSettings() const { language, proxyUrl: storeProxyUrl, userName, theme, setTheme } = useSettings()
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
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()
@@ -101,27 +103,6 @@ const GeneralSettings: FC = () => {
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.font_size.title')}</SettingRowTitle>
<Slider
style={{ width: 290 }}
value={fontSizeValue}
onChange={(value) => setFontSizeValue(value)}
onChangeComplete={(value) => {
dispatch(setFontSize(value))
console.debug('set font size', value)
}}
min={12}
max={18}
step={1}
marks={{
12: <span style={{ fontSize: '12px' }}>A</span>,
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
18: <span style={{ fontSize: '18px' }}>A</span>
}}
/>
</SettingRow>
<SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle> <SettingRowTitle>{t('settings.proxy.title')}</SettingRowTitle>
<Input <Input
@@ -134,6 +115,27 @@ const GeneralSettings: FC = () => {
/> />
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
<HStack gap="5px">
<Button onClick={backup} icon={<SaveOutlined />}>
</Button>
<Button onClick={restore} icon={<FolderOpenOutlined />}>
</Button>
</HStack>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
<HStack gap="5px">
<Button onClick={reset} danger>
{t('settings.general.reset.button')}
</Button>
</HStack>
</SettingRow>
<SettingDivider />
</SettingContainer> </SettingContainer>
) )
} }

View File

@@ -0,0 +1,48 @@
import MinApp from '@renderer/components/MinApp'
import { MinAppType, Provider } from '@renderer/types'
import { Button } from 'antd'
import { FC } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import { SettingSubtitle } from '..'
interface Props {
provider: Provider
}
const GraphRAGSettings: FC<Props> = ({ provider }) => {
const apiUrl = provider.apiHost
const modalId = provider.models.filter((model) => model.id.includes('global'))[0]?.id
const { t } = useTranslation()
const onShowGraphRAG = async () => {
const { appPath } = await window.api.getAppInfo()
const url = `file://${appPath}/resources/graphrag.html?apiUrl=${apiUrl}&modelId=${modalId}`
const app: MinAppType = {
name: t('words.knowledgeGraph'),
logo: '',
url
}
MinApp.start(app)
}
if (!modalId) {
return null
}
return (
<Container>
<SettingSubtitle>{t('words.knowledgeGraph')}</SettingSubtitle>
<Button style={{ marginTop: 10 }} onClick={onShowGraphRAG}>
{t('words.visualization')}
</Button>
</Container>
)
}
const Container = styled.div``
export default GraphRAGSettings

View File

@@ -4,8 +4,7 @@ import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import styled from 'styled-components' import styled from 'styled-components'
import { SettingSubtitle } from '..' import { SettingHelpText, SettingHelpTextRow, SettingSubtitle } from '..'
import { HelpText, HelpTextRow } from '../ProviderSettings/ProviderSetting'
const OllamSettings: FC = () => { const OllamSettings: FC = () => {
const { keepAliveTime, setKeepAliveTime } = useOllamaSettings() const { keepAliveTime, setKeepAliveTime } = useOllamaSettings()
@@ -14,7 +13,7 @@ const OllamSettings: FC = () => {
return ( return (
<Container> <Container>
<SettingSubtitle>{t('ollama.keep_alive_time.title')}</SettingSubtitle> <SettingSubtitle style={{ marginBottom: 5 }}>{t('ollama.keep_alive_time.title')}</SettingSubtitle>
<InputNumber <InputNumber
style={{ width: '100%' }} style={{ width: '100%' }}
value={keepAliveMinutes} value={keepAliveMinutes}
@@ -23,9 +22,9 @@ const OllamSettings: FC = () => {
suffix={t('ollama.keep_alive_time.placeholder')} suffix={t('ollama.keep_alive_time.placeholder')}
step={5} step={5}
/> />
<HelpTextRow> <SettingHelpTextRow>
<HelpText>{t('ollama.keep_alive_time.description')}</HelpText> <SettingHelpText>{t('ollama.keep_alive_time.description')}</SettingHelpText>
</HelpTextRow> </SettingHelpTextRow>
</Container> </Container>
) )
} }

View File

@@ -19,9 +19,17 @@ 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'
import { SettingContainer, SettingSubtitle, SettingTitle } from '..' import {
SettingContainer,
SettingHelpLink,
SettingHelpText,
SettingHelpTextRow,
SettingSubtitle,
SettingTitle
} from '..'
import AddModelPopup from './AddModelPopup' import AddModelPopup from './AddModelPopup'
import EditModelsPopup from './EditModelsPopup' import EditModelsPopup from './EditModelsPopup'
import GraphRAGSettings from './GraphRAGSettings'
import OllamSettings from './OllamaSettings' import OllamSettings from './OllamaSettings'
interface Props { interface Props {
@@ -95,7 +103,7 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
</SettingTitle> </SettingTitle>
<Divider style={{ width: '100%', margin: '10px 0' }} /> <Divider style={{ width: '100%', margin: '10px 0' }} />
<SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle> <SettingSubtitle style={{ marginTop: 5 }}>{t('settings.provider.api_key')}</SettingSubtitle>
<Space.Compact style={{ width: '100%' }}> <Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input.Password <Input.Password
value={apiKey} value={apiKey}
placeholder={t('settings.provider.api_key')} placeholder={t('settings.provider.api_key')}
@@ -110,14 +118,14 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
</Button> </Button>
</Space.Compact> </Space.Compact>
{apiKeyWebsite && ( {apiKeyWebsite && (
<HelpTextRow> <SettingHelpTextRow>
<HelpLink target="_blank" href={apiKeyWebsite}> <SettingHelpLink target="_blank" href={apiKeyWebsite}>
{t('settings.provider.get_api_key')} {t('settings.provider.get_api_key')}
</HelpLink> </SettingHelpLink>
</HelpTextRow> </SettingHelpTextRow>
)} )}
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle> <SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
<Space.Compact style={{ width: '100%' }}> <Space.Compact style={{ width: '100%', marginTop: 5 }}>
<Input <Input
value={apiHost} value={apiHost}
placeholder={t('settings.provider.api_host')} placeholder={t('settings.provider.api_host')}
@@ -128,7 +136,10 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
{apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>} {apiEditable && <Button onClick={onReset}>{t('settings.provider.api.url.reset')}</Button>}
</Space.Compact> </Space.Compact>
{provider.id === 'ollama' && <OllamSettings />} {provider.id === 'ollama' && <OllamSettings />}
<SettingSubtitle>{t('common.models')}</SettingSubtitle> {provider.id === 'graphrag-kylin-mountain' && provider.models.length > 0 && (
<GraphRAGSettings provider={provider} />
)}
<SettingSubtitle style={{ marginBottom: 5 }}>{t('common.models')}</SettingSubtitle>
{Object.keys(modelGroups).map((group) => ( {Object.keys(modelGroups).map((group) => (
<Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small"> <Card key={group} type="inner" title={group} style={{ marginBottom: '10px' }} size="small">
{modelGroups[group].map((model) => ( {modelGroups[group].map((model) => (
@@ -145,18 +156,18 @@ const ProviderSetting: FC<Props> = ({ provider: _provider }) => {
</Card> </Card>
))} ))}
{docsWebsite && ( {docsWebsite && (
<HelpTextRow> <SettingHelpTextRow>
<HelpText>{t('settings.provider.docs_check')} </HelpText> <SettingHelpText>{t('settings.provider.docs_check')} </SettingHelpText>
<HelpLink target="_blank" href={docsWebsite}> <SettingHelpLink target="_blank" href={docsWebsite}>
{t(`provider.${provider.id}`)} {t(`provider.${provider.id}`) + ' '}
{t('common.docs')} {t('common.docs')}
</HelpLink> </SettingHelpLink>
<HelpText>{t('common.and')}</HelpText> <SettingHelpText>{t('common.and')}</SettingHelpText>
<HelpLink target="_blank" href={modelsWebsite}> <SettingHelpLink target="_blank" href={modelsWebsite}>
{t('common.models')} {t('common.models')}
</HelpLink> </SettingHelpLink>
<HelpText>{t('settings.provider.docs_more_details')}</HelpText> <SettingHelpText>{t('settings.provider.docs_more_details')}</SettingHelpText>
</HelpTextRow> </SettingHelpTextRow>
)} )}
<Flex gap={10} style={{ marginTop: '10px' }}> <Flex gap={10} style={{ marginTop: '10px' }}>
<Button type="primary" onClick={onManageModel} icon={<EditOutlined />}> <Button type="primary" onClick={onManageModel} icon={<EditOutlined />}>
@@ -184,24 +195,6 @@ const ModelListHeader = styled.div`
align-items: center; align-items: center;
` `
export const HelpTextRow = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding: 5px 0;
`
export const HelpText = styled.div`
font-size: 11px;
color: var(--color-text);
opacity: 0.4;
`
const HelpLink = styled(Link)`
font-size: 11px;
padding: 0 5px;
`
const RemoveIcon = styled(MinusCircleOutlined)` const RemoveIcon = styled(MinusCircleOutlined)`
font-size: 18px; font-size: 18px;
margin-left: 10px; margin-left: 10px;

View File

@@ -167,7 +167,7 @@ const ProviderList = styled.div`
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
height: calc(100vh - var(--navbar-height)); height: calc(100vh - var(--navbar-height));
overflow: scroll; overflow: auto;
padding: 8px; padding: 8px;
` `
@@ -182,11 +182,10 @@ const ProviderListItem = styled.div`
font-size: 14px; font-size: 14px;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
&:hover { &:hover {
background: var(--color-primary-mute); background: var(--color-background-soft);
} }
&.active { &.active {
background: var(--color-primary); background: var(--color-background-mute);
color: var(--color-white);
font-weight: bold !important; font-weight: bold !important;
} }
` `

View File

@@ -118,11 +118,10 @@ const MenuItem = styled.li`
opacity: 0.8; opacity: 0.8;
} }
&:hover { &:hover {
background: var(--color-primary-soft); background: var(--color-background-soft);
} }
&.active { &.active {
background: var(--color-primary); background: var(--color-background-mute);
color: var(--color-white);
} }
` `

View File

@@ -1,4 +1,5 @@
import { Divider } from 'antd' import { Divider } from 'antd'
import Link from 'antd/es/typography/Link'
import styled from 'styled-components' import styled from 'styled-components'
import SettingsPage from './SettingsPage' import SettingsPage from './SettingsPage'
@@ -51,4 +52,22 @@ export const SettingRowTitle = styled.div`
color: var(--color-text-1); color: var(--color-text-1);
` `
export const SettingHelpTextRow = styled.div`
display: flex;
flex-direction: row;
align-items: center;
padding: 5px 0;
`
export const SettingHelpText = styled.div`
font-size: 11px;
color: var(--color-text);
opacity: 0.4;
`
export const SettingHelpLink = styled(Link)`
font-size: 11px;
padding: 0 5px;
`
export default SettingsPage export default SettingsPage

View File

@@ -1,12 +1,6 @@
import { import { CheckOutlined, SendOutlined, SettingOutlined, SwapOutlined, WarningOutlined } from '@ant-design/icons'
CheckOutlined,
CopyOutlined,
SendOutlined,
SettingOutlined,
SwapOutlined,
WarningOutlined
} from '@ant-design/icons'
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar' import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
import CopyIcon from '@renderer/components/Icons/CopyIcon'
import { useDefaultModel } from '@renderer/hooks/useAssistant' import { useDefaultModel } from '@renderer/hooks/useAssistant'
import { fetchTranslate } from '@renderer/services/api' import { fetchTranslate } from '@renderer/services/api'
import { getDefaultAssistant } from '@renderer/services/assistant' import { getDefaultAssistant } from '@renderer/services/assistant'
@@ -211,7 +205,7 @@ const TranslatePage: FC = () => {
<CopyButton <CopyButton
onClick={onCopy} onClick={onCopy}
disabled={!result} disabled={!result}
icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyOutlined />} icon={copied ? <CheckOutlined style={{ color: 'var(--color-primary)' }} /> : <CopyIcon />}
/> />
</OutputContainer> </OutputContainer>
</TranslateInputWrapper> </TranslateInputWrapper>
@@ -257,7 +251,7 @@ const InputContainer = styled.div`
flex: 1; flex: 1;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
border: 1px solid var(--color-border); border: 1px solid var(--color-border-soft);
border-radius: 10px; border-radius: 10px;
` `

View File

@@ -23,8 +23,7 @@ const AntdProvider: FC<PropsWithChildren> = ({ children }) => {
} }
}, },
token: { token: {
colorPrimary: '#00b96b', colorPrimary: '#00b96b'
borderRadius: 8
} }
}}> }}>
{children} {children}

View File

@@ -56,24 +56,36 @@ export default class ProviderSDK {
})) }))
if (this.isAnthropic) { if (this.isAnthropic) {
await this.anthropicSdk.messages return new Promise<void>((resolve, reject) => {
.stream({ const stream = this.anthropicSdk.messages
model: model.id, .stream({
messages: [systemMessage, ...userMessages].filter(Boolean) as MessageParam[], model: model.id,
max_tokens: maxTokens || DEFAULT_MAX_TOKENS, messages: userMessages.filter(Boolean) as MessageParam[],
temperature: assistant?.settings?.temperature max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
}) temperature: assistant?.settings?.temperature,
.on('text', (text) => onChunk({ text: text || '' })) system: assistant.prompt,
.on('finalMessage', (message) => stream: true
onChunk({
usage: {
prompt_tokens: message.usage.input_tokens,
completion_tokens: message.usage.output_tokens,
total_tokens: sum(Object.values(message.usage))
}
}) })
) .on('text', (text) => {
return 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))
})
} }
if (this.isGemini) { if (this.isGemini) {
@@ -140,9 +152,10 @@ export default class ProviderSDK {
if (this.isAnthropic) { if (this.isAnthropic) {
const response = await this.anthropicSdk.messages.create({ const response = await this.anthropicSdk.messages.create({
model: model.id, model: model.id,
messages: messages as MessageParam[], messages: messages.filter((m) => m.role === 'user') as MessageParam[],
max_tokens: 4096, max_tokens: 4096,
temperature: assistant?.settings?.temperature, temperature: assistant?.settings?.temperature,
system: assistant.prompt,
stream: false stream: false
}) })
@@ -179,7 +192,7 @@ export default class ProviderSDK {
const model = getTopNamingModel() || assistant.model || getDefaultModel() const model = getTopNamingModel() || assistant.model || getDefaultModel()
const userMessages = takeRight(messages, 5).map((message) => ({ const userMessages = takeRight(messages, 5).map((message) => ({
role: 'user', role: message.role,
content: message.content content: message.content
})) }))
@@ -190,10 +203,11 @@ export default class ProviderSDK {
if (this.isAnthropic) { if (this.isAnthropic) {
const message = await this.anthropicSdk.messages.create({ const message = await this.anthropicSdk.messages.create({
messages: [systemMessage, ...userMessages] as Anthropic.Messages.MessageParam[], messages: userMessages as Anthropic.Messages.MessageParam[],
model: model.id, model: model.id,
system: systemMessage.content,
stream: false, stream: false,
max_tokens: 50 max_tokens: 4096
}) })
return message.content[0].type === 'text' ? message.content[0].text : null return message.content[0].type === 'text' ? message.content[0].text : null

View File

@@ -123,7 +123,7 @@ export async function fetchMessagesSummary({ messages, assistant }: { messages:
const providerSdk = new ProviderSDK(provider) const providerSdk = new ProviderSDK(provider)
try { try {
return await providerSdk.summaries(messages, assistant) return await providerSdk.summaries(filterMessages(messages), assistant)
} catch (error: any) { } catch (error: any) {
return null return null
} }

View File

@@ -0,0 +1,74 @@
import i18n from '@renderer/i18n'
import dayjs from 'dayjs'
import localforage from 'localforage'
export async function backup() {
const indexedKeys = await localforage.keys()
const version = 1
const time = new Date().getTime()
const data = {
time,
version,
localStorage,
indexedDB: [] as { key: string; value: any }[]
}
for (const key of indexedKeys) {
data.indexedDB.push({
key,
value: await localforage.getItem(key)
})
}
const filename = `cherry-studio.${dayjs().format('YYYYMMDD')}.bak`
const fileContnet = JSON.stringify(data)
const file = await window.api.compress(fileContnet)
window.api.saveFile(filename, file)
}
export async function restore() {
const file = await window.api.openFile()
if (file) {
try {
const content = await window.api.decompress(file.content)
const data = JSON.parse(content)
if (data.version === 1) {
localStorage.setItem('persist:cherry-studio', data.localStorage['persist:cherry-studio'])
for (const { key, value } of data.indexedDB) {
await localforage.setItem(key, value)
}
window.message.success({ content: i18n.t('message.restore.success'), key: 'restore' })
setTimeout(() => window.api.reload(), 1500)
} else {
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
}
} catch (error) {
console.error(error)
window.message.error({ content: i18n.t('error.backup.file_format'), key: 'restore' })
}
}
}
export async function reset() {
window.modal.confirm({
title: i18n.t('common.warning'),
content: i18n.t('message.reset.confirm.content'),
onOk: async () => {
window.modal.confirm({
title: i18n.t('message.reset.double.confirm.title'),
content: i18n.t('message.reset.double.confirm.content'),
onOk: async () => {
await localStorage.clear()
await localforage.clear()
window.api.reload()
}
})
}
})
}

View File

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

View File

@@ -121,6 +121,15 @@ const initialState: LlmState = {
isSystem: true, isSystem: true,
enabled: false enabled: false
}, },
{
id: 'minimax',
name: 'MiniMax',
apiKey: '',
apiHost: 'https://api.minimax.chat/v1/',
models: SYSTEM_MODELS.minimax.filter((m) => m.enabled),
isSystem: true,
enabled: false
},
{ {
id: 'aihubmix', id: 'aihubmix',
name: 'AiHubMix', name: 'AiHubMix',

View File

@@ -343,6 +343,26 @@ const migrateConfig = {
] ]
} }
} }
},
'22': (state: RootState) => {
return {
...state,
llm: {
...state.llm,
providers: [
...state.llm.providers,
{
id: 'minimax',
name: 'MiniMax',
apiKey: '',
apiHost: 'https://api.minimax.chat/v1/',
models: SYSTEM_MODELS.minimax.filter((m) => m.enabled),
isSystem: true,
enabled: false
}
]
}
}
} }
} }

View File

@@ -4,11 +4,13 @@ import Logo from '@renderer/assets/images/logo.png'
export interface RuntimeState { export interface RuntimeState {
avatar: string avatar: string
generating: boolean generating: boolean
minappShow: boolean
} }
const initialState: RuntimeState = { const initialState: RuntimeState = {
avatar: Logo, avatar: Logo,
generating: false generating: false,
minappShow: false
} }
const runtimeSlice = createSlice({ const runtimeSlice = createSlice({
@@ -20,10 +22,13 @@ const runtimeSlice = createSlice({
}, },
setGenerating: (state, action: PayloadAction<boolean>) => { setGenerating: (state, action: PayloadAction<boolean>) => {
state.generating = action.payload state.generating = action.payload
},
setMinappShow: (state, action: PayloadAction<boolean>) => {
state.minappShow = action.payload
} }
} }
}) })
export const { setAvatar, setGenerating } = runtimeSlice.actions export const { setAvatar, setGenerating, setMinappShow } = runtimeSlice.actions
export default runtimeSlice.reducer export default runtimeSlice.reducer

View File

@@ -75,3 +75,9 @@ export type Agent = {
export type Suggestion = { export type Suggestion = {
content: string content: string
} }
export type MinAppType = {
name: string
logo: string
url: string
}