Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45dd76e281 | ||
|
|
568d4814e3 | ||
|
|
9468f3b511 | ||
|
|
04af940144 | ||
|
|
e33d9ac0ae | ||
|
|
cd835b7c36 | ||
|
|
dd4239da87 | ||
|
|
41c3895da4 | ||
|
|
2e9c7d0830 | ||
|
|
8ea73e14c9 | ||
|
|
3791556b13 | ||
|
|
e0dab5cf5b | ||
|
|
1785e7df0a | ||
|
|
6cb1846b23 | ||
|
|
21243579b3 | ||
|
|
0d2ad2e4c3 | ||
|
|
071a3950cd | ||
|
|
dc6066b74c | ||
|
|
ce55d8d0e7 | ||
|
|
d4ae321cd2 | ||
|
|
89dd35c98d | ||
|
|
b8c70a3061 | ||
|
|
968a749aaa | ||
|
|
e2fc593624 | ||
|
|
0e1674ce6c |
@@ -63,7 +63,6 @@ electronDownload:
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
全新的智能体界面 by @cawabj
|
||||
新增绘图模块
|
||||
文件管理界面优化
|
||||
修复可以同时启动多个应用问题
|
||||
支持聊天气泡样式和简洁样式切换
|
||||
支持导出对话为 Word 文档
|
||||
错误修复
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "0.8.6",
|
||||
"version": "0.8.9",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -39,12 +39,14 @@
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"archiver": "^7.0.1",
|
||||
"docx": "^9.0.2",
|
||||
"electron-log": "^5.1.5",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.3.9",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
"html2canvas": "^1.4.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"officeparser": "^4.1.1",
|
||||
"unzipper": "^0.12.3",
|
||||
"webdav": "4.11.4"
|
||||
@@ -60,6 +62,7 @@
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
"@types/node": "^18.19.9",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import fs from 'node:fs'
|
||||
|
||||
import { app } from 'electron'
|
||||
import Store from 'electron-store'
|
||||
import path from 'path'
|
||||
|
||||
import { getDataPath } from './utils'
|
||||
|
||||
const isDev = process.env.NODE_ENV === 'development'
|
||||
|
||||
isDev && app.setPath('userData', app.getPath('userData') + 'Dev')
|
||||
|
||||
const getDataPath = () => {
|
||||
const dataPath = path.join(app.getPath('userData'), 'Data')
|
||||
if (!fs.existsSync(dataPath)) {
|
||||
fs.mkdirSync(dataPath, { recursive: true })
|
||||
}
|
||||
return dataPath
|
||||
if (isDev) {
|
||||
app.setPath('userData', app.getPath('userData') + 'Dev')
|
||||
}
|
||||
|
||||
export const DATA_PATH = getDataPath()
|
||||
|
||||
export const appConfig = new Store()
|
||||
|
||||
export const titleBarOverlayDark = {
|
||||
height: 40,
|
||||
color: '#00000000',
|
||||
|
||||
@@ -3,7 +3,7 @@ import { app, BrowserWindow } from 'electron'
|
||||
import installExtension, { REDUX_DEVTOOLS } from 'electron-devtools-installer'
|
||||
|
||||
import { registerIpc } from './ipc'
|
||||
import { registerZoomShortcut } from './shortcut'
|
||||
import { registerZoomShortcut } from './services/ShortcutService'
|
||||
import { updateUserDataPath } from './utils/upgrade'
|
||||
import { createMainWindow } from './window'
|
||||
|
||||
|
||||
@@ -2,20 +2,22 @@ import path from 'node:path'
|
||||
|
||||
import { BrowserWindow, ipcMain, session, shell } from 'electron'
|
||||
|
||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import AppUpdater from './services/AppUpdater'
|
||||
import BackupManager from './services/BackupManager'
|
||||
import FileManager from './services/FileManager'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { ExportService } from './services/ExportService'
|
||||
import FileStorage from './services/FileStorage'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
import { createMinappWindow } from './window'
|
||||
|
||||
const fileManager = new FileManager()
|
||||
const fileManager = new FileStorage()
|
||||
const backupManager = new BackupManager()
|
||||
const exportService = new ExportService(fileManager)
|
||||
|
||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const { autoUpdater } = new AppUpdater(mainWindow)
|
||||
|
||||
// IPC
|
||||
ipcMain.handle('app:info', () => ({
|
||||
version: app.getVersion(),
|
||||
isPackaged: app.isPackaged,
|
||||
@@ -23,23 +25,36 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
filesPath: path.join(app.getPath('userData'), 'Data', 'Files')
|
||||
}))
|
||||
|
||||
ipcMain.handle('open-website', (_, url: string) => {
|
||||
shell.openExternal(url)
|
||||
ipcMain.handle('app:proxy', (_, proxy: string) => session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {}))
|
||||
ipcMain.handle('app:reload', () => mainWindow.reload())
|
||||
ipcMain.handle('open:website', (_, url: string) => shell.openExternal(url))
|
||||
|
||||
// theme
|
||||
ipcMain.handle('app:set-theme', (_, theme: 'light' | 'dark') => {
|
||||
configManager.setTheme(theme)
|
||||
mainWindow?.setTitleBarOverlay &&
|
||||
mainWindow.setTitleBarOverlay(theme === 'dark' ? titleBarOverlayDark : titleBarOverlayLight)
|
||||
})
|
||||
|
||||
ipcMain.handle('set-proxy', (_, proxy: string) => {
|
||||
session.defaultSession.setProxy(proxy ? { proxyRules: proxy } : {})
|
||||
// check for update
|
||||
ipcMain.handle('app:check-for-update', async () => {
|
||||
return {
|
||||
currentVersion: autoUpdater.currentVersion,
|
||||
update: await autoUpdater.checkForUpdates()
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('reload', () => mainWindow.reload())
|
||||
|
||||
// zip
|
||||
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
|
||||
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
|
||||
|
||||
// backup
|
||||
ipcMain.handle('backup:backup', backupManager.backup)
|
||||
ipcMain.handle('backup:restore', backupManager.restore)
|
||||
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
|
||||
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
|
||||
|
||||
// file
|
||||
ipcMain.handle('file:open', fileManager.open)
|
||||
ipcMain.handle('file:save', fileManager.save)
|
||||
ipcMain.handle('file:select', fileManager.selectFile)
|
||||
@@ -54,7 +69,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('file:saveImage', fileManager.saveImage)
|
||||
ipcMain.handle('file:base64Image', fileManager.base64Image)
|
||||
ipcMain.handle('file:download', fileManager.downloadFile)
|
||||
ipcMain.handle('file:copy', fileManager.copyFile)
|
||||
|
||||
// minapp
|
||||
ipcMain.handle('minapp', (_, args) => {
|
||||
createMinappWindow({
|
||||
url: args.url,
|
||||
@@ -66,17 +83,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
})
|
||||
})
|
||||
|
||||
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 () => {
|
||||
return {
|
||||
currentVersion: autoUpdater.currentVersion,
|
||||
update: await autoUpdater.checkForUpdates()
|
||||
}
|
||||
})
|
||||
// export
|
||||
ipcMain.handle('export:word', exportService.exportToWord)
|
||||
}
|
||||
|
||||
19
src/main/services/ConfigManager.ts
Normal file
19
src/main/services/ConfigManager.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import Store from 'electron-store'
|
||||
|
||||
export class ConfigManager {
|
||||
private store: Store
|
||||
|
||||
constructor() {
|
||||
this.store = new Store()
|
||||
}
|
||||
|
||||
getTheme(): 'light' | 'dark' {
|
||||
return this.store.get('theme', 'light') as 'light' | 'dark'
|
||||
}
|
||||
|
||||
setTheme(theme: 'light' | 'dark') {
|
||||
this.store.set('theme', theme)
|
||||
}
|
||||
}
|
||||
|
||||
export const configManager = new ConfigManager()
|
||||
222
src/main/services/ExportService.ts
Normal file
222
src/main/services/ExportService.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/* eslint-disable no-case-declarations */
|
||||
// ExportService
|
||||
|
||||
import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, ShadingType, TextRun } from 'docx'
|
||||
import { dialog } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import MarkdownIt from 'markdown-it'
|
||||
|
||||
import FileStorage from './FileStorage'
|
||||
|
||||
export class ExportService {
|
||||
private fileManager: FileStorage
|
||||
private md: MarkdownIt
|
||||
|
||||
constructor(fileManager: FileStorage) {
|
||||
this.fileManager = fileManager
|
||||
this.md = new MarkdownIt()
|
||||
}
|
||||
|
||||
private convertMarkdownToDocxElements(markdown: string) {
|
||||
const tokens = this.md.parse(markdown, {})
|
||||
const elements: any[] = []
|
||||
let listLevel = 0
|
||||
|
||||
const processInlineTokens = (tokens: any[]): TextRun[] => {
|
||||
const runs: TextRun[] = []
|
||||
for (const token of tokens) {
|
||||
switch (token.type) {
|
||||
case 'text':
|
||||
runs.push(new TextRun(token.content))
|
||||
break
|
||||
case 'strong':
|
||||
runs.push(new TextRun({ text: token.content, bold: true }))
|
||||
break
|
||||
case 'em':
|
||||
runs.push(new TextRun({ text: token.content, italics: true }))
|
||||
break
|
||||
case 'code_inline':
|
||||
runs.push(new TextRun({ text: token.content, font: 'Consolas', size: 20 }))
|
||||
break
|
||||
}
|
||||
}
|
||||
return runs
|
||||
}
|
||||
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i]
|
||||
|
||||
switch (token.type) {
|
||||
case 'heading_open':
|
||||
// 获取标题级别 (h1 -> h6)
|
||||
const level = parseInt(token.tag.slice(1)) as 1 | 2 | 3 | 4 | 5 | 6
|
||||
const headingText = tokens[i + 1].content
|
||||
elements.push(
|
||||
new Paragraph({
|
||||
text: headingText,
|
||||
heading: HeadingLevel[`HEADING_${level}`],
|
||||
spacing: {
|
||||
before: 240,
|
||||
after: 120
|
||||
}
|
||||
})
|
||||
)
|
||||
i += 2 // 跳过内容标记和闭合标记
|
||||
break
|
||||
|
||||
case 'paragraph_open':
|
||||
const inlineTokens = tokens[i + 1].children || []
|
||||
elements.push(
|
||||
new Paragraph({
|
||||
children: processInlineTokens(inlineTokens),
|
||||
spacing: {
|
||||
before: 120,
|
||||
after: 120
|
||||
}
|
||||
})
|
||||
)
|
||||
i += 2
|
||||
break
|
||||
|
||||
case 'bullet_list_open':
|
||||
listLevel++
|
||||
break
|
||||
|
||||
case 'bullet_list_close':
|
||||
listLevel--
|
||||
break
|
||||
|
||||
case 'list_item_open':
|
||||
const itemInlineTokens = tokens[i + 2].children || []
|
||||
elements.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({ text: '•', bold: true }),
|
||||
new TextRun({ text: '\t' }),
|
||||
...processInlineTokens(itemInlineTokens)
|
||||
],
|
||||
indent: {
|
||||
left: listLevel * 720
|
||||
}
|
||||
})
|
||||
)
|
||||
i += 3
|
||||
break
|
||||
|
||||
case 'fence': // 代码块
|
||||
const codeLines = token.content.split('\n')
|
||||
elements.push(
|
||||
new Paragraph({
|
||||
children: codeLines.map(
|
||||
(line) =>
|
||||
new TextRun({
|
||||
text: line + '\n',
|
||||
font: 'Consolas',
|
||||
size: 20,
|
||||
break: 1
|
||||
})
|
||||
),
|
||||
shading: {
|
||||
type: ShadingType.SOLID,
|
||||
color: 'F5F5F5'
|
||||
},
|
||||
spacing: {
|
||||
before: 120,
|
||||
after: 120
|
||||
},
|
||||
border: {
|
||||
top: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
|
||||
bottom: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
|
||||
left: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
|
||||
right: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' }
|
||||
}
|
||||
})
|
||||
)
|
||||
break
|
||||
|
||||
case 'hr':
|
||||
elements.push(
|
||||
new Paragraph({
|
||||
children: [new TextRun({ text: '─'.repeat(50), color: '999999' })],
|
||||
alignment: AlignmentType.CENTER
|
||||
})
|
||||
)
|
||||
break
|
||||
|
||||
case 'blockquote_open':
|
||||
const quoteText = tokens[i + 2].content
|
||||
elements.push(
|
||||
new Paragraph({
|
||||
children: [
|
||||
new TextRun({
|
||||
text: quoteText,
|
||||
italics: true
|
||||
})
|
||||
],
|
||||
indent: {
|
||||
left: 720
|
||||
},
|
||||
border: {
|
||||
left: {
|
||||
style: BorderStyle.SINGLE,
|
||||
size: 3,
|
||||
color: 'CCCCCC'
|
||||
}
|
||||
},
|
||||
spacing: {
|
||||
before: 120,
|
||||
after: 120
|
||||
}
|
||||
})
|
||||
)
|
||||
i += 3
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return elements
|
||||
}
|
||||
|
||||
public exportToWord = async (_: Electron.IpcMainInvokeEvent, markdown: string, fileName: string): Promise<void> => {
|
||||
try {
|
||||
const elements = this.convertMarkdownToDocxElements(markdown)
|
||||
|
||||
const doc = new Document({
|
||||
styles: {
|
||||
paragraphStyles: [
|
||||
{
|
||||
id: 'Normal',
|
||||
name: 'Normal',
|
||||
run: {
|
||||
size: 24,
|
||||
font: 'Arial'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
properties: {},
|
||||
children: elements
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const buffer = await Packer.toBuffer(doc)
|
||||
|
||||
const filePath = dialog.showSaveDialogSync({
|
||||
title: '保存文件',
|
||||
filters: [{ name: 'Word Document', extensions: ['docx'] }],
|
||||
defaultPath: fileName
|
||||
})
|
||||
|
||||
if (filePath) {
|
||||
await this.fileManager.writeFile(_, filePath, buffer)
|
||||
Logger.info('[ExportService] Document exported successfully')
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[ExportService] Export to Word failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { documentExts } from '@main/constant'
|
||||
import { documentExts, imageExts } from '@main/constant'
|
||||
import { getFileType } from '@main/utils/file'
|
||||
import { FileType } from '@types'
|
||||
import * as crypto from 'crypto'
|
||||
@@ -19,7 +19,7 @@ import * as path from 'path'
|
||||
import { chdir } from 'process'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
class FileManager {
|
||||
class FileStorage {
|
||||
private storageDir = path.join(app.getPath('userData'), 'Data', 'Files')
|
||||
private tempDir = path.join(app.getPath('temp'), 'CherryStudio')
|
||||
|
||||
@@ -119,6 +119,31 @@ class FileManager {
|
||||
return Promise.all(fileMetadataPromises)
|
||||
}
|
||||
|
||||
private async compressImage(sourcePath: string, destPath: string): Promise<void> {
|
||||
try {
|
||||
const stats = fs.statSync(sourcePath)
|
||||
const fileSizeInMB = stats.size / (1024 * 1024)
|
||||
|
||||
// 如果图片大于1MB才进行压缩
|
||||
if (fileSizeInMB > 1) {
|
||||
try {
|
||||
await fs.promises.copyFile(sourcePath, destPath)
|
||||
logger.info('[FileStorage] Image compressed successfully:', sourcePath)
|
||||
} catch (jimpError) {
|
||||
logger.error('[FileStorage] Image compression failed:', jimpError)
|
||||
await fs.promises.copyFile(sourcePath, destPath)
|
||||
}
|
||||
} else {
|
||||
// 小图片直接复制
|
||||
await fs.promises.copyFile(sourcePath, destPath)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('[FileStorage] Image handling failed:', error)
|
||||
// 错误情况下直接复制原文件
|
||||
await fs.promises.copyFile(sourcePath, destPath)
|
||||
}
|
||||
}
|
||||
|
||||
public uploadFile = async (_: Electron.IpcMainInvokeEvent, file: FileType): Promise<FileType> => {
|
||||
const duplicateFile = await this.findDuplicateFile(file.path)
|
||||
|
||||
@@ -128,10 +153,18 @@ class FileManager {
|
||||
|
||||
const uuid = uuidv4()
|
||||
const origin_name = path.basename(file.path)
|
||||
const ext = path.extname(origin_name)
|
||||
const ext = path.extname(origin_name).toLowerCase()
|
||||
const destPath = path.join(this.storageDir, uuid + ext)
|
||||
|
||||
await fs.promises.copyFile(file.path, destPath)
|
||||
logger.info('[FileStorage] Uploading file:', file.path)
|
||||
|
||||
// 根据文件类型选择处理方式
|
||||
if (imageExts.includes(ext)) {
|
||||
await this.compressImage(file.path, destPath)
|
||||
} else {
|
||||
await fs.promises.copyFile(file.path, destPath)
|
||||
}
|
||||
|
||||
const stats = await fs.promises.stat(destPath)
|
||||
const fileType = getFileType(ext)
|
||||
|
||||
@@ -265,7 +298,7 @@ class FileManager {
|
||||
fileName: string,
|
||||
content: string,
|
||||
options?: SaveDialogOptions
|
||||
): Promise<void> => {
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
|
||||
title: '保存文件',
|
||||
@@ -276,8 +309,11 @@ class FileManager {
|
||||
if (!result.canceled && result.filePath) {
|
||||
await writeFileSync(result.filePath, content, { encoding: 'utf-8' })
|
||||
}
|
||||
|
||||
return result.filePath
|
||||
} catch (err) {
|
||||
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -372,7 +408,7 @@ class FileManager {
|
||||
|
||||
return fileMetadata
|
||||
} catch (error) {
|
||||
logger.error('[FileManager] Download file error:', error)
|
||||
logger.error('[FileStorage] Download file error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -395,6 +431,25 @@ class FileManager {
|
||||
|
||||
return mimeToExtension[mimeType] || '.bin'
|
||||
}
|
||||
|
||||
public copyFile = async (_: Electron.IpcMainInvokeEvent, id: string, destPath: string): Promise<void> => {
|
||||
try {
|
||||
const sourcePath = path.join(this.storageDir, id)
|
||||
|
||||
// 确保目标目录存在
|
||||
const destDir = path.dirname(destPath)
|
||||
if (!fs.existsSync(destDir)) {
|
||||
await fs.promises.mkdir(destDir, { recursive: true })
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
await fs.promises.copyFile(sourcePath, destPath)
|
||||
logger.info('[FileStorage] File copied successfully:', { from: sourcePath, to: destPath })
|
||||
} catch (error) {
|
||||
logger.error('[FileStorage] Copy file failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FileManager
|
||||
export default FileStorage
|
||||
53
src/main/services/ShortcutService.ts
Normal file
53
src/main/services/ShortcutService.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { BrowserWindow, globalShortcut } from 'electron'
|
||||
|
||||
export function registerZoomShortcut(mainWindow: BrowserWindow) {
|
||||
const registerShortcuts = () => {
|
||||
// 注册放大快捷键 (Ctrl+Plus 或 Cmd+Plus)
|
||||
globalShortcut.register('CommandOrControl+=', () => {
|
||||
if (mainWindow) {
|
||||
const currentZoom = mainWindow.webContents.getZoomFactor()
|
||||
const newZoom = currentZoom + 0.1
|
||||
// Prevent zoom factor from exceeding reasonable limits
|
||||
if (newZoom <= 5.0) {
|
||||
mainWindow.webContents.setZoomFactor(newZoom)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 注册缩小快捷键 (Ctrl+Minus 或 Cmd+Minus)
|
||||
globalShortcut.register('CommandOrControl+-', () => {
|
||||
if (mainWindow) {
|
||||
const currentZoom = mainWindow.webContents.getZoomFactor()
|
||||
const newZoom = currentZoom - 0.1
|
||||
// Prevent zoom factor from going below 0.1
|
||||
if (newZoom >= 0.1) {
|
||||
mainWindow.webContents.setZoomFactor(newZoom)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 注册重置缩放快捷键 (Ctrl+0 或 Cmd+0)
|
||||
globalShortcut.register('CommandOrControl+0', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.setZoomFactor(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const unregisterShortcuts = () => {
|
||||
globalShortcut.unregister('CommandOrControl+=')
|
||||
globalShortcut.unregister('CommandOrControl+-')
|
||||
globalShortcut.unregister('CommandOrControl+0')
|
||||
}
|
||||
|
||||
// 当窗口获得焦点时注册快捷键
|
||||
mainWindow.on('focus', registerShortcuts)
|
||||
|
||||
// 当窗口失去焦点时注销快捷键
|
||||
mainWindow.on('blur', unregisterShortcuts)
|
||||
|
||||
// 初始注册(如果窗口已经处于焦点状态)
|
||||
if (mainWindow.isFocused()) {
|
||||
registerShortcuts()
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { BrowserWindow, globalShortcut } from 'electron'
|
||||
|
||||
export function registerZoomShortcut(mainWindow: BrowserWindow) {
|
||||
// 注册放大快捷键 (Ctrl+Plus 或 Cmd+Plus)
|
||||
globalShortcut.register('CommandOrControl+=', () => {
|
||||
if (mainWindow) {
|
||||
const currentZoom = mainWindow.webContents.getZoomFactor()
|
||||
mainWindow.webContents.setZoomFactor(currentZoom + 0.1)
|
||||
}
|
||||
})
|
||||
|
||||
// 注册缩小快捷键 (Ctrl+Minus 或 Cmd+Minus)
|
||||
globalShortcut.register('CommandOrControl+-', () => {
|
||||
if (mainWindow) {
|
||||
const currentZoom = mainWindow.webContents.getZoomFactor()
|
||||
mainWindow.webContents.setZoomFactor(currentZoom - 0.1)
|
||||
}
|
||||
})
|
||||
|
||||
// 注册重置缩放快捷键 (Ctrl+0 或 Cmd+0)
|
||||
globalShortcut.register('CommandOrControl+0', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.setZoomFactor(1)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
|
||||
import { app } from 'electron'
|
||||
@@ -5,3 +6,11 @@ import { app } from 'electron'
|
||||
export function getResourcePath() {
|
||||
return path.join(app.getAppPath(), 'resources')
|
||||
}
|
||||
|
||||
export function getDataPath() {
|
||||
const dataPath = path.join(app.getPath('userData'), 'Data')
|
||||
if (!fs.existsSync(dataPath)) {
|
||||
fs.mkdirSync(dataPath, { recursive: true })
|
||||
}
|
||||
return dataPath
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
|
||||
import icon from '../../build/icon.png?asset'
|
||||
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { titleBarOverlayDark, titleBarOverlayLight } from './config'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
|
||||
export function createMainWindow() {
|
||||
// Load the previous state with fallback to defaults
|
||||
@@ -13,7 +14,7 @@ export function createMainWindow() {
|
||||
defaultHeight: 670
|
||||
})
|
||||
|
||||
const theme = appConfig.get('theme') || 'light'
|
||||
const theme = configManager.getTheme()
|
||||
|
||||
// Create the browser window.
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
16
src/preload/index.d.ts
vendored
16
src/preload/index.d.ts
vendored
@@ -20,8 +20,10 @@ declare global {
|
||||
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>
|
||||
zip: {
|
||||
compress: (text: string) => Promise<Buffer>
|
||||
decompress: (text: Buffer) => Promise<string>
|
||||
}
|
||||
backup: {
|
||||
backup: (fileName: string, data: string, destinationPath?: string) => Promise<Readable>
|
||||
restore: (backupPath: string) => Promise<string>
|
||||
@@ -39,10 +41,18 @@ declare global {
|
||||
create: (fileName: string) => Promise<string>
|
||||
write: (filePath: string, data: Uint8Array | string) => Promise<void>
|
||||
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
|
||||
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void
|
||||
save: (
|
||||
path: string,
|
||||
content: string | NodeJS.ArrayBufferView,
|
||||
options?: SaveDialogOptions
|
||||
) => Promise<string | null>
|
||||
saveImage: (name: string, data: string) => void
|
||||
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
|
||||
download: (url: string) => Promise<FileType | null>
|
||||
copy: (fileId: string, destPath: string) => Promise<void>
|
||||
}
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => Promise<void>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@ import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||
// Custom APIs for renderer
|
||||
const api = {
|
||||
getAppInfo: () => ipcRenderer.invoke('app:info'),
|
||||
checkForUpdate: () => ipcRenderer.invoke('check-for-update'),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open-website', url),
|
||||
setProxy: (proxy: string) => ipcRenderer.invoke('set-proxy', proxy),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('set-theme', theme),
|
||||
reload: () => ipcRenderer.invoke('app:reload'),
|
||||
setProxy: (proxy: string) => ipcRenderer.invoke('app:proxy', proxy),
|
||||
checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('app:set-theme', theme),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url),
|
||||
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
||||
reload: () => ipcRenderer.invoke('reload'),
|
||||
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text),
|
||||
zip: {
|
||||
compress: (text: string) => ipcRenderer.invoke('zip:compress', text),
|
||||
decompress: (text: Buffer) => ipcRenderer.invoke('zip:decompress', text)
|
||||
},
|
||||
backup: {
|
||||
backup: (fileName: string, data: string, destinationPath?: string) =>
|
||||
ipcRenderer.invoke('backup:backup', fileName, data, destinationPath),
|
||||
@@ -36,7 +38,11 @@ const api = {
|
||||
selectFolder: () => ipcRenderer.invoke('file:selectFolder'),
|
||||
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data),
|
||||
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId),
|
||||
download: (url: string) => ipcRenderer.invoke('file:download', url)
|
||||
download: (url: string) => ipcRenderer.invoke('file:download', url),
|
||||
copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath)
|
||||
},
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
--color-icon-white: #ffffff;
|
||||
--color-border: #ffffff24;
|
||||
--color-border-soft: #ffffff20;
|
||||
--color-border-mute: #ffffff11;
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #323232;
|
||||
@@ -51,6 +52,11 @@
|
||||
--assistants-width: 275px;
|
||||
--topic-list-width: 275px;
|
||||
--settings-width: 250px;
|
||||
|
||||
--chat-background: #111111;
|
||||
--chat-background-user: #28b561;
|
||||
--chat-background-assistant: #2c2c2c;
|
||||
--chat-text-user: var(--color-black);
|
||||
}
|
||||
|
||||
body[theme-mode='light'] {
|
||||
@@ -83,6 +89,7 @@ body[theme-mode='light'] {
|
||||
--color-icon-white: #000000;
|
||||
--color-border: #00000028;
|
||||
--color-border-soft: #00000028;
|
||||
--color-border-mute: #00000011;
|
||||
--color-error: #f44336;
|
||||
--color-link: #1677ff;
|
||||
--color-code-background: #e3e3e3;
|
||||
@@ -91,6 +98,11 @@ body[theme-mode='light'] {
|
||||
|
||||
--navbar-background-mac: rgba(255, 255, 255, 0.6);
|
||||
--navbar-background: rgba(255, 255, 255);
|
||||
|
||||
--chat-background: #f3f3f3;
|
||||
--chat-background-user: #95ec69;
|
||||
--chat-background-assistant: #ffffff;
|
||||
--chat-text-user: var(--color-text);
|
||||
}
|
||||
|
||||
*,
|
||||
@@ -156,7 +168,6 @@ body,
|
||||
body[os='mac'] {
|
||||
#content-container {
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
}
|
||||
|
||||
span {
|
||||
word-break: break-all;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
code {
|
||||
@@ -107,6 +107,8 @@
|
||||
background: var(--color-background-mute);
|
||||
padding: 3px 5px;
|
||||
border-radius: 5px;
|
||||
word-break: keep-all;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
||||
@@ -57,7 +57,7 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style }) =>
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={<TranslationOutlined />}
|
||||
icon={<TranslationOutlined style={{ fontSize: 14 }} />}
|
||||
onClick={handleTranslate}
|
||||
disabled={disabled || isTranslating}
|
||||
loading={isTranslating}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { FileSearchOutlined, FolderOutlined, PictureOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
@@ -21,6 +22,7 @@ const Sidebar: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { windowStyle } = useSettings()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
|
||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
|
||||
@@ -86,6 +88,14 @@ const Sidebar: FC = () => {
|
||||
</Menus>
|
||||
</MainMenus>
|
||||
<Menus onClick={MinApp.onClose}>
|
||||
<Icon onClick={() => toggleTheme()}>
|
||||
{theme === 'dark' ? (
|
||||
<i className="iconfont icon-theme icon-dark1" />
|
||||
) : (
|
||||
<i className="iconfont icon-theme icon-theme-light" />
|
||||
)}
|
||||
</Icon>
|
||||
|
||||
<StyledLink onClick={() => to(isLocalAi ? '/settings/assistant' : '/settings/provider')}>
|
||||
<Icon className={pathname.startsWith('/settings') ? 'active' : ''}>
|
||||
<i className="iconfont icon-setting" />
|
||||
|
||||
@@ -8,7 +8,7 @@ export const isMac = platform === 'darwin'
|
||||
export const isWindows = platform === 'win32' || platform === 'win64'
|
||||
export const isLinux = platform === 'linux'
|
||||
|
||||
export const imageExts = ['.jpg', '.png', '.jpeg']
|
||||
export const imageExts = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||
export const documentExts = ['.pdf', '.docx', '.pptx', '.xlsx', '.odt', '.odp', '.ods']
|
||||
export const textExts = [
|
||||
'.txt', // 普通文本文件
|
||||
|
||||
@@ -28,3 +28,12 @@ export function useSettings() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useMessageStyle() {
|
||||
const { messageStyle } = useSettings()
|
||||
const isBubbleStyle = messageStyle === 'bubble'
|
||||
|
||||
return {
|
||||
isBubbleStyle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,10 @@
|
||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||
"upgrade.success.button": "Restart",
|
||||
"topic.added": "New topic added",
|
||||
"save.success.title": "Saved successfully"
|
||||
"save.success.title": "Saved successfully",
|
||||
"message.style": "Message Style",
|
||||
"message.style.bubble": "Bubble",
|
||||
"message.style.plain": "Plain"
|
||||
},
|
||||
"chat": {
|
||||
"save": "Save",
|
||||
@@ -81,6 +84,7 @@
|
||||
"topics.export.title": "Export",
|
||||
"topics.export.image": "Export as image",
|
||||
"topics.export.md": "Export as markdown",
|
||||
"topics.export.word": "Export as Word",
|
||||
"input.new_topic": "New Topic",
|
||||
"input.topics": " Topics ",
|
||||
"input.clear": "Clear",
|
||||
@@ -154,7 +158,7 @@
|
||||
"guidance_scale_tip": "Classifier Free Guidance. How close you want the model to stick to your prompt when looking for a related image to show you",
|
||||
"negative_prompt": "Negative Prompt",
|
||||
"negative_prompt_tip": "Describe what you don't want included in the image",
|
||||
"prompt_placeholder": "Describe the image you want to create, e.g. 'A serene lake at sunset with mountains in the background'",
|
||||
"prompt_placeholder": "Describe the image you want to create, e.g. A serene lake at sunset with mountains in the background",
|
||||
"regenerate.confirm": "This will replace your existing generated images. Do you want to continue?"
|
||||
},
|
||||
"files": {
|
||||
@@ -196,7 +200,8 @@
|
||||
"edit.message.empty.content": "Conversation input content cannot be empty",
|
||||
"edit.model.select.title": "Select Model",
|
||||
"edit.settings.hide_preset_messages": "Hide Preset Message",
|
||||
"search.no_results": "No results found"
|
||||
"search.no_results": "No results found",
|
||||
"sorting.title": "Sorting"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "MinApp"
|
||||
@@ -383,6 +388,16 @@
|
||||
"words": {
|
||||
"knowledgeGraph": "Knowledge Graph",
|
||||
"visualization": "Visualization"
|
||||
},
|
||||
"export": {
|
||||
"attached_files": "Attached Files",
|
||||
"user": "User",
|
||||
"assistant": "Assistant",
|
||||
"created": "Created",
|
||||
"last_updated": "Last Updated",
|
||||
"messages": "Messages",
|
||||
"conversation_details": "Conversation Details",
|
||||
"conversation_history": "Conversation History"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,10 @@
|
||||
"upgrade.success.content": "重启用以完成升级",
|
||||
"upgrade.success.button": "重启",
|
||||
"topic.added": "话题添加成功",
|
||||
"save.success.title": "保存成功"
|
||||
"save.success.title": "保存成功",
|
||||
"message.style": "消息样式",
|
||||
"message.style.bubble": "气泡",
|
||||
"message.style.plain": "简洁"
|
||||
},
|
||||
"chat": {
|
||||
"save": "保存",
|
||||
@@ -81,6 +84,7 @@
|
||||
"topics.export.title": "导出",
|
||||
"topics.export.image": "导出为图片",
|
||||
"topics.export.md": "导出为 Markdown",
|
||||
"topics.export.word": "导出为 Word",
|
||||
"input.new_topic": "新话题",
|
||||
"input.topics": " 话题 ",
|
||||
"input.clear": "清空消息",
|
||||
@@ -154,7 +158,7 @@
|
||||
"guidance_scale_tip": "无分类器指导。控制模型在寻找相关图像时对提示词的遵循程度",
|
||||
"negative_prompt": "反向提示词",
|
||||
"negative_prompt_tip": "描述你不想在图片中出现的内容",
|
||||
"prompt_placeholder": "描述你想创建的图片,例如:'一个宁静的湖泊,夕阳西下,远处是群山'",
|
||||
"prompt_placeholder": "描述你想创建的图片,例如:一个宁静的湖泊,夕阳西下,远处是群山",
|
||||
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?"
|
||||
},
|
||||
"files": {
|
||||
@@ -196,7 +200,8 @@
|
||||
"edit.message.empty.content": "会话输入内容不能为空",
|
||||
"edit.model.select.title": "选择模型",
|
||||
"edit.settings.hide_preset_messages": "隐藏预设消息",
|
||||
"search.no_results": "没有找到相关智能体"
|
||||
"search.no_results": "没有找到相关智能体",
|
||||
"sorting.title": "排序"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "小程序"
|
||||
@@ -383,6 +388,16 @@
|
||||
"words": {
|
||||
"knowledgeGraph": "知识图谱",
|
||||
"visualization": "可视化"
|
||||
},
|
||||
"export": {
|
||||
"attached_files": "附件",
|
||||
"user": "用户",
|
||||
"assistant": "助手",
|
||||
"created": "创建时间",
|
||||
"last_updated": "最后更新",
|
||||
"messages": "消息数",
|
||||
"conversation_details": "会话详情",
|
||||
"conversation_history": "会话历史"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,10 @@
|
||||
"upgrade.success.content": "請重新啟動應用以完成升級",
|
||||
"upgrade.success.button": "重新啟動",
|
||||
"topic.added": "新話題已添加",
|
||||
"save.success.title": "保存成功"
|
||||
"save.success.title": "保存成功",
|
||||
"message.style": "消息樣式",
|
||||
"message.style.bubble": "氣泡",
|
||||
"message.style.plain": "簡潔"
|
||||
},
|
||||
"chat": {
|
||||
"save": "保存",
|
||||
@@ -81,6 +84,7 @@
|
||||
"topics.export.title": "匯出",
|
||||
"topics.export.image": "匯出為圖片",
|
||||
"topics.export.md": "匯出為 Markdown",
|
||||
"topics.export.word": "導出為 Word",
|
||||
"input.new_topic": "新話題",
|
||||
"input.topics": " 話題 ",
|
||||
"input.clear": "清除",
|
||||
@@ -154,7 +158,7 @@
|
||||
"guidance_scale_tip": "無分類器指導。控制模型在尋找相關圖像時對提示詞的遵循程度",
|
||||
"negative_prompt": "反向提示詞",
|
||||
"negative_prompt_tip": "描述你不想在圖片中出現的內容",
|
||||
"prompt_placeholder": "描述你想創建的圖片,例如:'一個寧靜的湖泊,夕陽西下,遠處是群山'",
|
||||
"prompt_placeholder": "描述你想創建的圖片,例如:一個寧靜的湖泊,夕陽西下,遠處是群山",
|
||||
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?"
|
||||
},
|
||||
"files": {
|
||||
@@ -196,7 +200,8 @@
|
||||
"edit.message.empty.content": "會話輸入內容不能為空",
|
||||
"edit.model.select.title": "選擇模型",
|
||||
"edit.settings.hide_preset_messages": "隱藏預設消息",
|
||||
"search.no_results": "沒有找到相關智能體"
|
||||
"search.no_results": "沒有找到相關智能體",
|
||||
"sorting.title": "排序"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "小程序"
|
||||
@@ -383,6 +388,16 @@
|
||||
"words": {
|
||||
"knowledgeGraph": "知識圖譜",
|
||||
"visualization": "可視化"
|
||||
},
|
||||
"export": {
|
||||
"attached_files": "附件",
|
||||
"user": "用戶",
|
||||
"assistant": "助手",
|
||||
"created": "創建時間",
|
||||
"last_updated": "最後更新",
|
||||
"messages": "訊息數",
|
||||
"conversation_details": "會話詳情",
|
||||
"conversation_history": "會話歷史"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
|
||||
import DragableList from '@renderer/components/DragableList'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { Button, Col, Typography } from 'antd'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import AddAgentPopup from './components/AddAgentPopup'
|
||||
import AgentCard from './components/AgentCard'
|
||||
|
||||
interface Props {
|
||||
onClick?: (agent: Agent) => void
|
||||
cardStyle?: 'new' | 'old'
|
||||
}
|
||||
|
||||
const Agents: React.FC<Props> = ({ onClick, cardStyle = 'old' }) => {
|
||||
const { t } = useTranslation()
|
||||
const { agents, removeAgent, updateAgents } = useAgents()
|
||||
const [dragging, setDragging] = useState(false)
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(agent: Agent) => {
|
||||
window.modal.confirm({
|
||||
centered: true,
|
||||
content: t('agents.delete.popup.content'),
|
||||
onOk: () => removeAgent(agent.id)
|
||||
})
|
||||
},
|
||||
[removeAgent, t]
|
||||
)
|
||||
|
||||
if (cardStyle === 'new') {
|
||||
return (
|
||||
<>
|
||||
{agents.map((agent) => {
|
||||
const dropdownMenuItems = [
|
||||
{
|
||||
key: 'edit',
|
||||
label: t('agents.edit.title'),
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
|
||||
},
|
||||
{
|
||||
key: 'create',
|
||||
label: t('agents.add.button'),
|
||||
icon: <PlusOutlined />,
|
||||
onClick: () => createAssistantFromAgent(agent)
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: t('common.delete'),
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => handleDelete(agent)
|
||||
}
|
||||
]
|
||||
|
||||
const contextMenuItems = [
|
||||
{
|
||||
label: t('agents.edit.title'),
|
||||
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
|
||||
},
|
||||
{
|
||||
label: t('agents.add.button'),
|
||||
onClick: () => createAssistantFromAgent(agent)
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
onClick: () => handleDelete(agent)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Col span={8} xxl={6} key={agent.id}>
|
||||
<AgentCard
|
||||
agent={agent}
|
||||
onClick={() => onClick?.(agent)}
|
||||
contextMenu={contextMenuItems}
|
||||
menuItems={dropdownMenuItems}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div style={{ paddingBottom: dragging ? 30 : 0 }}>
|
||||
<Typography.Title level={5} style={{ marginBottom: 16 }}>
|
||||
{t('agents.my_agents')}
|
||||
</Typography.Title>
|
||||
{agents.length > 0 && (
|
||||
<DragableList
|
||||
list={agents}
|
||||
onUpdate={updateAgents}
|
||||
onDragStart={() => setDragging(true)}
|
||||
onDragEnd={() => setDragging(false)}>
|
||||
{(agent: Agent) => {
|
||||
const dropdownMenuItems = [
|
||||
{
|
||||
key: 'edit',
|
||||
label: t('agents.edit.title'),
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
|
||||
},
|
||||
{
|
||||
key: 'create',
|
||||
label: t('agents.add.button'),
|
||||
icon: <PlusOutlined />,
|
||||
onClick: () => createAssistantFromAgent(agent)
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: t('common.delete'),
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => handleDelete(agent)
|
||||
}
|
||||
]
|
||||
|
||||
const contextMenuItems = [
|
||||
{
|
||||
label: t('agents.edit.title'),
|
||||
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
|
||||
},
|
||||
{
|
||||
label: t('agents.add.button'),
|
||||
onClick: () => createAssistantFromAgent(agent)
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
onClick: () => handleDelete(agent)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<AgentCard
|
||||
agent={agent}
|
||||
onClick={() => onClick?.(agent)}
|
||||
contextMenu={contextMenuItems}
|
||||
menuItems={dropdownMenuItems}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
</DragableList>
|
||||
)}
|
||||
{!dragging && (
|
||||
<Button
|
||||
type="dashed"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => AddAgentPopup.show()}
|
||||
style={{ borderRadius: 20, height: 34 }}>
|
||||
{t('agents.add.title')}
|
||||
</Button>
|
||||
)}
|
||||
<div style={{ height: 10 }} />
|
||||
</div>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 10px 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - var(--navbar-height));
|
||||
min-width: var(--assistants-width);
|
||||
max-width: var(--assistants-width);
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
border-radius: 3px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
background: var(--color-scrollbar-thumb);
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
&:hover::-webkit-scrollbar-thumb {
|
||||
background: var(--color-scrollbar-thumb);
|
||||
}
|
||||
`
|
||||
|
||||
export default Agents
|
||||
@@ -1,4 +1,4 @@
|
||||
import { PlusOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import SystemAgents from '@renderer/config/agents.json'
|
||||
@@ -13,9 +13,8 @@ import ReactMarkdown from 'react-markdown'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { groupTranslations } from './agentGroupTranslations'
|
||||
import Agents from './Agents'
|
||||
import AddAgentPopup from './components/AddAgentPopup'
|
||||
import AgentCard from './components/AgentCard'
|
||||
import MyAgents from './components/MyAgents'
|
||||
|
||||
const { Title } = Typography
|
||||
|
||||
@@ -45,7 +44,7 @@ const AgentsPage: FC = () => {
|
||||
const { t, i18n } = useTranslation()
|
||||
|
||||
const filteredAgentGroups = useMemo(() => {
|
||||
const groups = search.trim() ? {} : { 我的: [] }
|
||||
const groups = { 我的: [] }
|
||||
|
||||
if (!search.trim()) {
|
||||
Object.entries(agentGroups).forEach(([group, agents]) => {
|
||||
@@ -113,8 +112,8 @@ const AgentsPage: FC = () => {
|
||||
|
||||
const tabItems = useMemo(() => {
|
||||
let groups = Object.keys(filteredAgentGroups)
|
||||
groups = groups.filter((g) => g !== '我的' && g !== '办公')
|
||||
groups = ['我的', '办公', ...groups]
|
||||
|
||||
groups = groups.includes('办公') ? [groups[0], '办公', ...groups.slice(1)] : groups
|
||||
|
||||
return groups.map((group, i) => {
|
||||
const id = String(i + 1)
|
||||
@@ -128,17 +127,12 @@ const AgentsPage: FC = () => {
|
||||
<Title level={5} key={group} style={{ marginBottom: 16 }}>
|
||||
{localizedGroupName}
|
||||
</Title>
|
||||
<Row gutter={[25, 25]}>
|
||||
<Row gutter={[20, 20]}>
|
||||
{group === '我的' ? (
|
||||
<>
|
||||
<Col span={8} xxl={6}>
|
||||
<AddAgentCard onClick={() => AddAgentPopup.show()} />
|
||||
</Col>
|
||||
<Agents onClick={onAddAgentConfirm} cardStyle="new" />
|
||||
</>
|
||||
<MyAgents onClick={onAddAgentConfirm} search={search} />
|
||||
) : (
|
||||
filteredAgentGroups[group]?.map((agent, index) => (
|
||||
<Col span={8} xxl={6} key={group + index}>
|
||||
<Col span={6} key={group + index}>
|
||||
<AgentCard onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent))} agent={agent as any} />
|
||||
</Col>
|
||||
))
|
||||
@@ -148,10 +142,10 @@ const AgentsPage: FC = () => {
|
||||
)
|
||||
}
|
||||
})
|
||||
}, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm])
|
||||
}, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm, search])
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
||||
{t('agents.title')}
|
||||
@@ -173,7 +167,7 @@ const AgentsPage: FC = () => {
|
||||
<ContentContainer id="content-container">
|
||||
<AssistantsContainer>
|
||||
{tabItems.length > 0 ? (
|
||||
<Tabs tabPosition="left" animated items={tabItems} />
|
||||
<Tabs tabPosition="right" animated items={tabItems} />
|
||||
) : (
|
||||
<EmptyView>
|
||||
<Empty description={t('agents.search.no_results')} />
|
||||
@@ -181,11 +175,11 @@ const AgentsPage: FC = () => {
|
||||
)}
|
||||
</AssistantsContainer>
|
||||
</ContentContainer>
|
||||
</StyledContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
@@ -199,6 +193,7 @@ const ContentContainer = styled.div`
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
padding-left: 0;
|
||||
`
|
||||
|
||||
const AssistantsContainer = styled.div`
|
||||
@@ -211,7 +206,8 @@ const AssistantsContainer = styled.div`
|
||||
const TabContent = styled(Scrollbar)`
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
padding: 10px 10px 10px 15px;
|
||||
margin-right: 4px;
|
||||
margin-right: -4px;
|
||||
padding-bottom: 20px !important;
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
@@ -235,7 +231,11 @@ const Tabs = styled(TabsAntd)`
|
||||
flex: 1;
|
||||
flex-direction: row-reverse;
|
||||
.ant-tabs-tabpane {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
.ant-tabs-nav {
|
||||
min-width: 140px;
|
||||
max-width: 140px;
|
||||
}
|
||||
.ant-tabs-nav-list {
|
||||
padding: 10px 8px;
|
||||
@@ -247,8 +247,9 @@ const Tabs = styled(TabsAntd)`
|
||||
margin: 0 !important;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 5px !important;
|
||||
font-size: 14px;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
justify-content: left;
|
||||
padding: 7px 12px !important;
|
||||
&:hover {
|
||||
color: var(--color-text) !important;
|
||||
background-color: var(--color-background-soft);
|
||||
@@ -259,8 +260,8 @@ const Tabs = styled(TabsAntd)`
|
||||
border-right: none;
|
||||
}
|
||||
.ant-tabs-content-holder {
|
||||
border-left: none;
|
||||
border-right: 0.5px solid var(--color-border);
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
border-right: none;
|
||||
}
|
||||
.ant-tabs-ink-bar {
|
||||
display: none;
|
||||
@@ -275,33 +276,4 @@ const Tabs = styled(TabsAntd)`
|
||||
}
|
||||
`
|
||||
|
||||
const AddAgentCard = styled(({ onClick, className }: { onClick: () => void; className?: string }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className={className} onClick={onClick}>
|
||||
<PlusOutlined style={{ fontSize: 24 }} />
|
||||
<span style={{ marginTop: 10 }}>{t('agents.add.title')}</span>
|
||||
</div>
|
||||
)
|
||||
})`
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color-background);
|
||||
border-radius: 15px;
|
||||
border: 1px dashed var(--color-border);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: var(--color-text-soft);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
`
|
||||
|
||||
export default AgentsPage
|
||||
|
||||
41
src/renderer/src/pages/agents/components/AddAgentCard.tsx
Normal file
41
src/renderer/src/pages/agents/components/AddAgentCard.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { PlusOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface AddAgentCardProps {
|
||||
onClick: () => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
const AddAgentCard = ({ onClick, className }: AddAgentCardProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<StyledCard className={className} onClick={onClick}>
|
||||
<PlusOutlined style={{ fontSize: 24 }} />
|
||||
<span style={{ marginTop: 10 }}>{t('agents.add.title')}</span>
|
||||
</StyledCard>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledCard = styled.div`
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color-background);
|
||||
border-radius: 15px;
|
||||
border: 1px dashed var(--color-border);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
color: var(--color-text-soft);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
`
|
||||
|
||||
export default AddAgentCard
|
||||
@@ -17,9 +17,63 @@ interface Props {
|
||||
}[]
|
||||
}
|
||||
|
||||
const AgentCard: React.FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => {
|
||||
const emoji = agent.emoji || getLeadingEmoji(agent.name)
|
||||
const prompt = (agent.description || agent.prompt).substring(0, 100).replace(/\\n/g, '')
|
||||
const content = (
|
||||
<Container onClick={onClick}>
|
||||
{emoji && <BannerBackground className="banner-background">{emoji}</BannerBackground>}
|
||||
<EmojiContainer className="emoji-container">{emoji}</EmojiContainer>
|
||||
{menuItems && (
|
||||
<MenuContainer onClick={(e) => e.stopPropagation()}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: menuItems.map((item) => ({
|
||||
...item,
|
||||
onClick: (e) => {
|
||||
e.domEvent.stopPropagation()
|
||||
e.domEvent.preventDefault()
|
||||
setTimeout(() => {
|
||||
item.onClick()
|
||||
}, 0)
|
||||
}
|
||||
}))
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="bottomRight">
|
||||
<EllipsisOutlined style={{ cursor: 'pointer', fontSize: 20 }} />
|
||||
</Dropdown>
|
||||
</MenuContainer>
|
||||
)}
|
||||
<CardInfo className="card-info">
|
||||
<AgentName>{agent.name}</AgentName>
|
||||
<AgentPrompt className="agent-prompt">{prompt}...</AgentPrompt>
|
||||
</CardInfo>
|
||||
</Container>
|
||||
)
|
||||
|
||||
if (contextMenu) {
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: contextMenu.map((item) => ({
|
||||
key: item.label,
|
||||
label: item.label,
|
||||
onClick: () => item.onClick()
|
||||
}))
|
||||
}}
|
||||
trigger={['contextMenu']}>
|
||||
{content}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
height: 220px;
|
||||
height: 180px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@@ -36,7 +90,7 @@ const Container = styled.div`
|
||||
&::before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
height: 70px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -51,41 +105,21 @@ const Container = styled.div`
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&:hover::before {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
}
|
||||
|
||||
&:hover .card-info {
|
||||
transform: translateY(-15px);
|
||||
padding: 0 20px;
|
||||
|
||||
.agent-prompt {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .emoji-container {
|
||||
transform: scale(0.6);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
&:hover .banner-background {
|
||||
height: 100%;
|
||||
.agent-prompt {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
`
|
||||
|
||||
const EmojiContainer = styled.div`
|
||||
width: 70px;
|
||||
height: 70px;
|
||||
min-width: 70px;
|
||||
min-height: 70px;
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
min-width: 55px;
|
||||
min-height: 55px;
|
||||
background-color: var(--color-background);
|
||||
border-radius: 50%;
|
||||
border: 4px solid var(--color-border);
|
||||
margin-top: 20px;
|
||||
margin-top: 8px;
|
||||
transition: all 0.5s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -97,7 +131,7 @@ const CardInfo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 5px;
|
||||
transition: all 0.5s ease;
|
||||
padding: 0 15px;
|
||||
width: 100%;
|
||||
@@ -119,14 +153,14 @@ const AgentName = styled.span`
|
||||
|
||||
const AgentPrompt = styled.p`
|
||||
color: var(--color-text-soft);
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
max-width: 100%;
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
transition: all 0.5s ease;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
@@ -137,13 +171,13 @@ const BannerBackground = styled.div`
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
height: 70px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 500px;
|
||||
opacity: 0.1;
|
||||
filter: blur(10px);
|
||||
filter: blur(8px);
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
transition: all 0.5s ease;
|
||||
@@ -171,57 +205,4 @@ const MenuContainer = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const AgentCard: React.FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => {
|
||||
const emoji = agent.emoji || getLeadingEmoji(agent.name)
|
||||
const content = (
|
||||
<Container onClick={onClick}>
|
||||
{agent.emoji && <BannerBackground className="banner-background">{agent.emoji}</BannerBackground>}
|
||||
<EmojiContainer className="emoji-container">{emoji}</EmojiContainer>
|
||||
{menuItems && (
|
||||
<MenuContainer onClick={(e) => e.stopPropagation()}>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: menuItems.map((item) => ({
|
||||
...item,
|
||||
onClick: (e) => {
|
||||
e.domEvent.stopPropagation()
|
||||
e.domEvent.preventDefault()
|
||||
setTimeout(() => {
|
||||
item.onClick()
|
||||
}, 0)
|
||||
}
|
||||
}))
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="bottomRight">
|
||||
<EllipsisOutlined style={{ cursor: 'pointer' }} />
|
||||
</Dropdown>
|
||||
</MenuContainer>
|
||||
)}
|
||||
<CardInfo className="card-info">
|
||||
<AgentName>{agent.name}</AgentName>
|
||||
<AgentPrompt className="agent-prompt">{(agent.description || agent.prompt).substring(0, 100)}...</AgentPrompt>
|
||||
</CardInfo>
|
||||
</Container>
|
||||
)
|
||||
|
||||
if (contextMenu) {
|
||||
return (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: contextMenu.map((item) => ({
|
||||
key: item.label,
|
||||
label: item.label,
|
||||
onClick: () => item.onClick()
|
||||
}))
|
||||
}}
|
||||
trigger={['contextMenu']}>
|
||||
{content}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
export default AgentCard
|
||||
|
||||
100
src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx
Normal file
100
src/renderer/src/pages/agents/components/ManageAgentsPopup.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import { MenuOutlined } from '@ant-design/icons'
|
||||
import DragableList from '@renderer/components/DragableList'
|
||||
import { Box, HStack } from '@renderer/components/Layout'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { Empty, Modal } from 'antd'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const PopupContainer: React.FC = () => {
|
||||
const [open, setOpen] = useState(true)
|
||||
const { t } = useTranslation()
|
||||
const { agents, updateAgents } = useAgents()
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const onClose = async () => {
|
||||
ManageAgentsPopup.hide()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (agents.length === 0) {
|
||||
setOpen(false)
|
||||
}
|
||||
}, [agents])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t('agents.manage.title')}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
afterClose={onClose}
|
||||
footer={null}
|
||||
centered>
|
||||
<Container>
|
||||
{agents.length > 0 && (
|
||||
<DragableList list={agents} onUpdate={updateAgents}>
|
||||
{(item) => (
|
||||
<AgentItem>
|
||||
<Box mr={8}>
|
||||
{item.emoji} {item.name}
|
||||
</Box>
|
||||
<HStack gap="15px">
|
||||
<MenuOutlined style={{ cursor: 'move' }} />
|
||||
</HStack>
|
||||
</AgentItem>
|
||||
)}
|
||||
</DragableList>
|
||||
)}
|
||||
{agents.length === 0 && <Empty description="" />}
|
||||
</Container>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 12px 0;
|
||||
height: 50vh;
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const AgentItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
background-color: var(--color-background-soft);
|
||||
margin-bottom: 8px;
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
color: var(--color-icon);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
`
|
||||
|
||||
export default class ManageAgentsPopup {
|
||||
static topviewId = 0
|
||||
static hide() {
|
||||
TopView.hide('ManageAgentsPopup')
|
||||
}
|
||||
static show() {
|
||||
TopView.show(<PopupContainer />, 'ManageAgentsPopup')
|
||||
}
|
||||
}
|
||||
109
src/renderer/src/pages/agents/components/MyAgents.tsx
Normal file
109
src/renderer/src/pages/agents/components/MyAgents.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { DeleteOutlined, EditOutlined, PlusOutlined, SortAscendingOutlined } from '@ant-design/icons'
|
||||
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { Col } from 'antd'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import AddAgentCard from './AddAgentCard'
|
||||
import AddAgentPopup from './AddAgentPopup'
|
||||
import AgentCard from './AgentCard'
|
||||
import ManageAgentsPopup from './ManageAgentsPopup'
|
||||
|
||||
interface Props {
|
||||
onClick?: (agent: Agent) => void
|
||||
search?: string
|
||||
}
|
||||
|
||||
const MyAgents: React.FC<Props> = ({ onClick, search }) => {
|
||||
const { t } = useTranslation()
|
||||
const { agents, removeAgent } = useAgents()
|
||||
|
||||
const filteredAgents = useMemo(() => {
|
||||
if (!search?.trim()) return agents
|
||||
|
||||
return agents.filter(
|
||||
(agent) =>
|
||||
agent.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
agent.description?.toLowerCase().includes(search.toLowerCase())
|
||||
)
|
||||
}, [agents, search])
|
||||
|
||||
const handleDelete = useCallback(
|
||||
(agent: Agent) => {
|
||||
window.modal.confirm({
|
||||
centered: true,
|
||||
content: t('agents.delete.popup.content'),
|
||||
onOk: () => removeAgent(agent.id)
|
||||
})
|
||||
},
|
||||
[removeAgent, t]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{filteredAgents.map((agent) => {
|
||||
const dropdownMenuItems = [
|
||||
{
|
||||
key: 'edit',
|
||||
label: t('agents.edit.title'),
|
||||
icon: <EditOutlined />,
|
||||
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
|
||||
},
|
||||
{
|
||||
key: 'create',
|
||||
label: t('agents.add.button'),
|
||||
icon: <PlusOutlined />,
|
||||
onClick: () => createAssistantFromAgent(agent)
|
||||
},
|
||||
{
|
||||
key: 'sort',
|
||||
label: t('agents.sorting.title'),
|
||||
icon: <SortAscendingOutlined />,
|
||||
onClick: () => ManageAgentsPopup.show()
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: t('common.delete'),
|
||||
icon: <DeleteOutlined />,
|
||||
danger: true,
|
||||
onClick: () => handleDelete(agent)
|
||||
}
|
||||
]
|
||||
|
||||
const contextMenuItems = [
|
||||
{
|
||||
label: t('agents.edit.title'),
|
||||
onClick: () => AssistantSettingsPopup.show({ assistant: agent })
|
||||
},
|
||||
{
|
||||
label: t('agents.add.button'),
|
||||
onClick: () => createAssistantFromAgent(agent)
|
||||
},
|
||||
{
|
||||
label: t('common.delete'),
|
||||
onClick: () => handleDelete(agent)
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<Col span={6} key={agent.id}>
|
||||
<AgentCard
|
||||
agent={agent}
|
||||
onClick={() => onClick?.(agent)}
|
||||
contextMenu={contextMenuItems}
|
||||
menuItems={dropdownMenuItems}
|
||||
/>
|
||||
</Col>
|
||||
)
|
||||
})}
|
||||
<Col span={6}>
|
||||
<AddAgentCard onClick={() => AddAgentPopup.show()} />
|
||||
</Col>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyAgents
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { VStack } from '@renderer/components/Layout'
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||
@@ -35,7 +36,7 @@ const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props
|
||||
<ListContainer {...props}>
|
||||
<VStack alignItems="center">
|
||||
<Empty description={t('history.search.topics.empty')} />
|
||||
<Button style={{ width: 200, marginTop: 20 }} type="primary" onClick={onSearch}>
|
||||
<Button style={{ width: 200, marginTop: 20 }} type="primary" onClick={onSearch} icon={<SearchOutlined />}>
|
||||
{t('history.search.messages')}
|
||||
</Button>
|
||||
</VStack>
|
||||
@@ -63,6 +64,13 @@ const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props
|
||||
))}
|
||||
</ListItem>
|
||||
))}
|
||||
{keywords.length >= 2 && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', width: '100%' }}>
|
||||
<Button style={{ width: 200, marginTop: 20 }} type="primary" onClick={onSearch} icon={<SearchOutlined />}>
|
||||
{t('history.search.messages')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div style={{ minHeight: 30 }}></div>
|
||||
</ContainerWrapper>
|
||||
</ListContainer>
|
||||
|
||||
@@ -19,11 +19,11 @@ interface Props {
|
||||
|
||||
const Chat: FC<Props> = (props) => {
|
||||
const { assistant } = useAssistant(props.assistant.id)
|
||||
const { topicPosition } = useSettings()
|
||||
const { topicPosition, messageStyle } = useSettings()
|
||||
const { showTopics } = useShowTopics()
|
||||
|
||||
return (
|
||||
<Container id="chat">
|
||||
<Container id="chat" className={messageStyle}>
|
||||
<Main vertical flex={1} justify="space-between">
|
||||
<Messages
|
||||
key={props.activeTopic.id}
|
||||
@@ -52,7 +52,35 @@ const Container = styled.div`
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-background);
|
||||
&.bubble {
|
||||
background-color: var(--chat-background);
|
||||
.system-prompt {
|
||||
background-color: var(--chat-background-assistant);
|
||||
}
|
||||
.message-content-container {
|
||||
margin: 5px 0;
|
||||
border-radius: 8px;
|
||||
padding: 10px 15px 0 15px;
|
||||
}
|
||||
.message-user {
|
||||
.markdown,
|
||||
.anticon,
|
||||
.iconfont,
|
||||
.message-tokens {
|
||||
color: var(--chat-text-user);
|
||||
}
|
||||
.message-action-button:hover {
|
||||
background-color: var(--color-white-soft);
|
||||
}
|
||||
}
|
||||
#inputbar {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border-mute);
|
||||
background: var(--color-background);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const Main = styled(Flex)`
|
||||
|
||||
@@ -35,8 +35,9 @@ const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
margin: 10px 20px;
|
||||
margin-right: 0;
|
||||
padding: 10px 20px;
|
||||
background: var(--color-background);
|
||||
border-top: 1px solid var(--color-border-mute);
|
||||
`
|
||||
|
||||
export default AttachmentPreview
|
||||
|
||||
@@ -12,7 +12,7 @@ import { isVisionModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { addAssistantMessagesToTopic, getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
@@ -58,6 +58,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
const containerRef = useRef(null)
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
const { searching } = useRuntime()
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||
@@ -299,7 +300,7 @@ const Inputbar: FC<Props> = ({ assistant, setActiveTopic }) => {
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
variant="borderless"
|
||||
rows={1}
|
||||
rows={isBubbleStyle ? 2 : 1}
|
||||
ref={textareaRef}
|
||||
style={{ fontSize }}
|
||||
styles={{ textarea: TextareaStyle }}
|
||||
@@ -375,11 +376,6 @@ const Container = styled.div`
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
const TextareaStyle: CSSProperties = {
|
||||
paddingLeft: 0,
|
||||
padding: '10px 15px 8px'
|
||||
}
|
||||
|
||||
const InputBarContainer = styled.div`
|
||||
border: 1px solid var(--color-border-soft);
|
||||
transition: all 0.3s ease;
|
||||
@@ -388,6 +384,11 @@ const InputBarContainer = styled.div`
|
||||
border-radius: 10px;
|
||||
`
|
||||
|
||||
const TextareaStyle: CSSProperties = {
|
||||
paddingLeft: 0,
|
||||
padding: '10px 15px 8px'
|
||||
}
|
||||
|
||||
const Textarea = styled(TextArea)`
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
|
||||
@@ -2,12 +2,12 @@ import { FONT_FAMILY } from '@renderer/config/constant'
|
||||
import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useModel } from '@renderer/hooks/useModel'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { fetchChatCompletion } from '@renderer/services/ApiService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { estimateMessageUsage } from '@renderer/services/TokenService'
|
||||
import { Message, Topic } from '@renderer/types'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { classNames, runAsyncFunction } from '@renderer/utils'
|
||||
import { Divider } from 'antd'
|
||||
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -16,7 +16,7 @@ import styled from 'styled-components'
|
||||
import MessageContent from './MessageContent'
|
||||
import MessageHeader from './MessageHeader'
|
||||
import MessageMenubar from './MessageMenubar'
|
||||
import MessgeTokens from './MessageTokens'
|
||||
import MessageTokens from './MessageTokens'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
@@ -42,11 +42,13 @@ const MessageItem: FC<Props> = ({
|
||||
const { t } = useTranslation()
|
||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||
const model = useModel(message.modelId)
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
const { showMessageDivider, messageFont, fontSize } = useSettings()
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isLastMessage = index === 0
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
|
||||
const showMenubar = !message.status.includes('ing')
|
||||
|
||||
const fontFamily = useMemo(() => {
|
||||
@@ -54,6 +56,11 @@ const MessageItem: FC<Props> = ({
|
||||
}, [messageFont])
|
||||
|
||||
const messageBorder = showMessageDivider ? undefined : 'none'
|
||||
const messageBackground = isBubbleStyle
|
||||
? isAssistantMessage
|
||||
? 'var(--chat-background-assistant)'
|
||||
: 'var(--chat-background-user)'
|
||||
: undefined
|
||||
|
||||
const onEditMessage = useCallback(
|
||||
(msg: Message) => {
|
||||
@@ -131,13 +138,27 @@ const MessageItem: FC<Props> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageContainer key={message.id} className="message" ref={messageContainerRef}>
|
||||
<MessageContainer
|
||||
key={message.id}
|
||||
className={classNames({
|
||||
message: true,
|
||||
'message-assistant': isAssistantMessage,
|
||||
'message-user': !isAssistantMessage
|
||||
})}
|
||||
ref={messageContainerRef}
|
||||
style={isBubbleStyle ? { alignItems: isAssistantMessage ? 'start' : 'end' } : undefined}>
|
||||
<MessageHeader message={message} assistant={assistant} model={model} />
|
||||
<MessageContentContainer style={{ fontFamily, fontSize }}>
|
||||
<MessageContentContainer
|
||||
className="message-content-container"
|
||||
style={{ fontFamily, fontSize, background: messageBackground }}>
|
||||
<MessageContent message={message} model={model} />
|
||||
{showMenubar && (
|
||||
<MessageFooter style={{ border: messageBorder, flexDirection: isLastMessage ? 'row-reverse' : undefined }}>
|
||||
<MessgeTokens message={message} isLastMessage={isLastMessage} />
|
||||
<MessageFooter
|
||||
style={{
|
||||
border: messageBorder,
|
||||
flexDirection: isLastMessage || isBubbleStyle ? 'row-reverse' : undefined
|
||||
}}>
|
||||
<MessageTokens message={message} isLastMessage={isLastMessage} />
|
||||
<MessageMenubar
|
||||
message={message}
|
||||
model={model}
|
||||
@@ -179,6 +200,7 @@ const MessageContainer = styled.div`
|
||||
`
|
||||
|
||||
const MessageContentContainer = styled.div`
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
@@ -195,6 +217,7 @@ const MessageFooter = styled.div`
|
||||
padding: 2px 0;
|
||||
margin-top: 2px;
|
||||
border-top: 0.5px dashed var(--color-border);
|
||||
gap: 20px;
|
||||
`
|
||||
|
||||
export default memo(MessageItem)
|
||||
|
||||
@@ -42,6 +42,8 @@ const MessageContentLoading = styled.div`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 32px;
|
||||
margin-top: -5px;
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
export default React.memo(MessageContent)
|
||||
|
||||
@@ -4,12 +4,12 @@ import { startMinAppById } from '@renderer/config/minapps'
|
||||
import { getModelLogo } from '@renderer/config/models'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Assistant, Message, Model } from '@renderer/types'
|
||||
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
|
||||
import { Avatar } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { FC, useCallback, useMemo } from 'react'
|
||||
import { CSSProperties, FC, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -24,8 +24,7 @@ const MessageHeader: FC<Props> = ({ assistant, model, message }) => {
|
||||
const { theme } = useTheme()
|
||||
const { userName } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
|
||||
const avatarSource = useMemo(() => {
|
||||
if (isLocalAi) return AppLogo
|
||||
@@ -39,15 +38,23 @@ const MessageHeader: FC<Props> = ({ assistant, model, message }) => {
|
||||
return userName || t('common.you')
|
||||
}, [message.role, model?.id, model?.name, t, userName])
|
||||
|
||||
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
|
||||
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
|
||||
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
|
||||
|
||||
const showMiniApp = () => model?.provider && startMinAppById(model?.provider)
|
||||
|
||||
const avatarStyle: CSSProperties | undefined = isBubbleStyle
|
||||
? {
|
||||
flexDirection: isAssistantMessage ? 'row' : 'row-reverse',
|
||||
textAlign: isAssistantMessage ? 'left' : 'right'
|
||||
}
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<AvatarWrapper>
|
||||
<AvatarWrapper style={avatarStyle}>
|
||||
{isAssistantMessage ? (
|
||||
<Avatar
|
||||
src={avatarSource}
|
||||
@@ -79,25 +86,23 @@ const MessageHeader: FC<Props> = ({ assistant, model, message }) => {
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin-right: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-bottom: 4px;
|
||||
justify-content: space-between;
|
||||
`
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
`
|
||||
|
||||
const UserWrap = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-left: 12px;
|
||||
`
|
||||
|
||||
const UserName = styled.div`
|
||||
|
||||
@@ -96,27 +96,27 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onEdit}>
|
||||
<ActionButton className="message-action-button" onClick={onEdit}>
|
||||
<EditOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('common.copy')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onCopy}>
|
||||
<ActionButton className="message-action-button" onClick={onCopy}>
|
||||
{!copied && <i className="iconfont icon-copy"></i>}
|
||||
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{canRegenerate && (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onSelectModel}>
|
||||
<ActionButton className="message-action-button" onClick={onSelectModel}>
|
||||
<SyncOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isAssistantMessage && (
|
||||
<Tooltip title={t('chat.message.new.branch')} mouseEnterDelay={0.8}>
|
||||
<ActionButton onClick={onNewBranch}>
|
||||
<ActionButton className="message-action-button" onClick={onNewBranch}>
|
||||
<ForkOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
@@ -127,14 +127,14 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
onConfirm={() => onDeleteMessage?.(message)}>
|
||||
<Tooltip title={t('common.delete')} mouseEnterDelay={1}>
|
||||
<ActionButton>
|
||||
<ActionButton className="message-action-button">
|
||||
<DeleteOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
{!isUserMessage && (
|
||||
<Dropdown menu={{ items: dropdownItems }} trigger={['click']} placement="topRight" arrow>
|
||||
<ActionButton>
|
||||
<ActionButton className="message-action-button">
|
||||
<MenuOutlined />
|
||||
</ActionButton>
|
||||
</Dropdown>
|
||||
|
||||
@@ -15,7 +15,11 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({
|
||||
}
|
||||
|
||||
if (message.role === 'user') {
|
||||
return <MessageMetadata onClick={locateMessage}>Tokens: {message?.usage?.total_tokens}</MessageMetadata>
|
||||
return (
|
||||
<MessageMetadata className="message-tokens" onClick={locateMessage}>
|
||||
Tokens: {message?.usage?.total_tokens}
|
||||
</MessageMetadata>
|
||||
)
|
||||
}
|
||||
|
||||
if (isLastMessage && generating) {
|
||||
@@ -24,7 +28,7 @@ const MessgeTokens: React.FC<{ message: Message; isLastMessage: boolean }> = ({
|
||||
|
||||
if (message.role === 'assistant') {
|
||||
return (
|
||||
<MessageMetadata onClick={locateMessage}>
|
||||
<MessageMetadata className="message-tokens" onClick={locateMessage}>
|
||||
Tokens: {message?.usage?.total_tokens} | ↑{message?.usage?.prompt_tokens} | ↓{message?.usage?.completion_tokens}
|
||||
</MessageMetadata>
|
||||
)
|
||||
|
||||
@@ -217,7 +217,6 @@ const Container = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 10px 0;
|
||||
background-color: var(--color-background);
|
||||
padding-bottom: 20px;
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
@@ -18,7 +18,7 @@ const Prompt: FC<Props> = ({ assistant }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Container onClick={() => AssistantSettingsPopup.show({ assistant })}>
|
||||
<Container className="system-prompt" onClick={() => AssistantSettingsPopup.show({ assistant })}>
|
||||
<Text>{prompt}</Text>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -3,13 +3,11 @@ import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar
|
||||
import AssistantSettingsPopup from '@renderer/components/AssistantSettings'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { Switch } from 'antd'
|
||||
import { FC, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -24,7 +22,6 @@ interface Props {
|
||||
const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const { topicPosition } = useSettings()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
|
||||
@@ -63,12 +60,6 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</HStack>
|
||||
<HStack alignItems="center">
|
||||
<ThemeSwitch
|
||||
checkedChildren={<i className="iconfont icon-theme icon-dark1" />}
|
||||
unCheckedChildren={<i className="iconfont icon-theme icon-theme-light" />}
|
||||
checked={theme === 'dark'}
|
||||
onChange={toggleTheme}
|
||||
/>
|
||||
{topicPosition === 'right' && (
|
||||
<NewButton onClick={toggleShowTopics}>
|
||||
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
|
||||
@@ -118,12 +109,4 @@ const TitleText = styled.span`
|
||||
font-weight: 500;
|
||||
`
|
||||
|
||||
const ThemeSwitch = styled(Switch)`
|
||||
-webkit-app-region: no-drag;
|
||||
margin-right: 10px;
|
||||
.icon-theme {
|
||||
font-size: 14px;
|
||||
}
|
||||
`
|
||||
|
||||
export default HeaderNavbar
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
setFontSize,
|
||||
setMathEngine,
|
||||
setMessageFont,
|
||||
setMessageStyle,
|
||||
setPasteLongTextAsFile,
|
||||
setRenderInputMessageAsMarkdown,
|
||||
setShowInputEstimatedTokens,
|
||||
@@ -35,6 +36,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
const [maxTokens, setMaxTokens] = useState(assistant?.settings?.maxTokens ?? 0)
|
||||
const [streamOutput, setStreamOutput] = useState(assistant?.settings?.streamOutput ?? true)
|
||||
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
|
||||
const { messageStyle } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
@@ -214,6 +216,18 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall>
|
||||
<Select
|
||||
value={messageStyle}
|
||||
onChange={(value) => dispatch(setMessageStyle(value))}
|
||||
style={{ width: 100 }}
|
||||
size="small">
|
||||
<Select.Option value="plain">{t('message.message.style.plain')}</Select.Option>
|
||||
<Select.Option value="bubble">{t('message.message.style.bubble')}</Select.Option>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.math_engine')}</SettingRowTitleSmall>
|
||||
<Select
|
||||
|
||||
@@ -17,7 +17,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { exportTopicAsMarkdown } from '@renderer/utils/export'
|
||||
import { exportTopicAsMarkdown, topicToMarkdown } from '@renderer/utils/export'
|
||||
import { Dropdown, MenuProps } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { findIndex } from 'lodash'
|
||||
@@ -141,6 +141,14 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
||||
label: t('chat.topics.export.md'),
|
||||
key: 'markdown',
|
||||
onClick: () => exportTopicAsMarkdown(topic)
|
||||
},
|
||||
{
|
||||
label: t('chat.topics.export.word'),
|
||||
key: 'word',
|
||||
onClick: async () => {
|
||||
const markdown = await topicToMarkdown(topic)
|
||||
window.api.export.toWord(markdown, topic.name)
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ const Container = styled.div`
|
||||
max-width: var(--assistants-width);
|
||||
min-width: var(--assistants-width);
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
background-color: var(--color-background);
|
||||
overflow: hidden;
|
||||
.collapsed {
|
||||
width: 0;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'
|
||||
import DragableList from '@renderer/components/DragableList'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { Painting } from '@renderer/types'
|
||||
@@ -14,15 +15,22 @@ interface PaintingsListProps {
|
||||
selectedPainting: Painting
|
||||
onSelectPainting: (painting: Painting) => void
|
||||
onDeletePainting: (painting: Painting) => void
|
||||
onNewPainting: () => void
|
||||
}
|
||||
|
||||
const PaintingsList: FC<PaintingsListProps> = ({ paintings, selectedPainting, onSelectPainting, onDeletePainting }) => {
|
||||
const PaintingsList: FC<PaintingsListProps> = ({
|
||||
paintings,
|
||||
selectedPainting,
|
||||
onSelectPainting,
|
||||
onDeletePainting,
|
||||
onNewPainting
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const { updatePaintings } = usePaintings()
|
||||
|
||||
return (
|
||||
<Container style={{ paddingBottom: dragging ? 30 : 0 }}>
|
||||
<Container style={{ paddingBottom: dragging ? 80 : 10 }}>
|
||||
<DragableList
|
||||
list={paintings}
|
||||
onUpdate={updatePaintings}
|
||||
@@ -47,21 +55,27 @@ const PaintingsList: FC<PaintingsListProps> = ({ paintings, selectedPainting, on
|
||||
</CanvasWrapper>
|
||||
)}
|
||||
</DragableList>
|
||||
{!dragging && (
|
||||
<NewPaintingButton onClick={onNewPainting}>
|
||||
<PlusOutlined />
|
||||
</NewPaintingButton>
|
||||
)}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
const Container = styled(Scrollbar)`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: var(--color-background);
|
||||
max-width: 100px;
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
height: calc(100vh - var(--navbar-height));
|
||||
overflow-x: hidden;
|
||||
`
|
||||
|
||||
const CanvasWrapper = styled.div`
|
||||
@@ -116,4 +130,25 @@ const DeleteButton = styled.div.attrs({ className: 'delete-button' })`
|
||||
justify-content: center;
|
||||
`
|
||||
|
||||
const NewPaintingButton = styled.div`
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
min-height: 80px;
|
||||
margin-top: -10px;
|
||||
background-color: var(--color-background-soft);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border: 1px dashed var(--color-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-2);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-mute);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
`
|
||||
|
||||
export default PaintingsList
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Navbar, NavbarCenter, NavbarRight } from '@renderer/components/app/Navb
|
||||
import { VStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { TEXT_TO_IMAGES_MODELS } from '@renderer/config/models'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { usePaintings } from '@renderer/hooks/usePaintings'
|
||||
@@ -252,11 +253,13 @@ const PaintingsPage: FC = () => {
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('images.title')}</NavbarCenter>
|
||||
<NavbarRight style={{ justifyContent: 'flex-end' }}>
|
||||
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={() => setPainting(addPainting())}>
|
||||
{t('images.button.new.image')}
|
||||
</Button>
|
||||
</NavbarRight>
|
||||
{isMac && (
|
||||
<NavbarRight style={{ justifyContent: 'flex-end' }}>
|
||||
<Button size="small" className="nodrag" icon={<PlusOutlined />} onClick={() => setPainting(addPainting())}>
|
||||
{t('images.button.new.image')}
|
||||
</Button>
|
||||
</NavbarRight>
|
||||
)}
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<LeftContainer>
|
||||
@@ -379,7 +382,7 @@ const PaintingsPage: FC = () => {
|
||||
text={textareaRef.current?.resizableTextArea?.textArea?.value}
|
||||
onTranslated={handleTranslation}
|
||||
disabled={isLoading}
|
||||
style={{ marginRight: 6 }}
|
||||
style={{ marginRight: 6, borderRadius: '50%' }}
|
||||
/>
|
||||
<SendMessageButton sendMessage={onGenerate} disabled={isLoading} />
|
||||
</ToolbarMenu>
|
||||
@@ -391,6 +394,7 @@ const PaintingsPage: FC = () => {
|
||||
selectedPainting={painting}
|
||||
onSelectPainting={onSelectPainting}
|
||||
onDeletePainting={onDeletePainting}
|
||||
onNewPainting={() => setPainting(addPainting())}
|
||||
/>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
@@ -435,13 +439,11 @@ const MainContainer = styled.div`
|
||||
const InputContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100px;
|
||||
max-height: 100px;
|
||||
min-height: 95px;
|
||||
max-height: 95px;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-soft);
|
||||
border-top: 1px solid var(--color-border-soft);
|
||||
transition: all 0.3s ease;
|
||||
margin: 0 20px 15px 20px;
|
||||
border-radius: 10px;
|
||||
`
|
||||
|
||||
const Textarea = styled(TextArea)`
|
||||
@@ -462,7 +464,7 @@ const Toolbar = styled.div`
|
||||
justify-content: flex-end;
|
||||
padding: 0 8px;
|
||||
padding-bottom: 0;
|
||||
height: 36px;
|
||||
height: 40px;
|
||||
`
|
||||
|
||||
const ToolbarMenu = styled.div`
|
||||
|
||||
@@ -26,7 +26,7 @@ export async function restore() {
|
||||
const restoreData = await window.api.backup.restore(file.filePath)
|
||||
data = JSON.parse(restoreData)
|
||||
} else {
|
||||
data = JSON.parse(await window.api.decompress(file.content))
|
||||
data = JSON.parse(await window.api.zip.decompress(file.content))
|
||||
}
|
||||
|
||||
await handleData(data)
|
||||
|
||||
@@ -9,7 +9,8 @@ export const getModelUniqId = (m?: Model) => {
|
||||
export const hasModel = (m?: Model) => {
|
||||
const allModels = store
|
||||
.getState()
|
||||
.llm.providers.map((p) => p.models)
|
||||
.llm.providers.filter((p) => p.enabled)
|
||||
.map((p) => p.models)
|
||||
.flat()
|
||||
|
||||
return allModels.find((model) => model.id === m?.id)
|
||||
|
||||
@@ -24,7 +24,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 35,
|
||||
version: 37,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@@ -614,6 +614,14 @@ const migrateConfig = {
|
||||
'35': (state: RootState) => {
|
||||
state.settings.mathEngine = 'KaTeX'
|
||||
return state
|
||||
},
|
||||
'36': (state: RootState) => {
|
||||
state.settings.topicPosition = 'left'
|
||||
return state
|
||||
},
|
||||
'37': (state: RootState) => {
|
||||
state.settings.messageStyle = 'plain'
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface SettingsState {
|
||||
renderInputMessageAsMarkdown: boolean
|
||||
codeShowLineNumbers: boolean
|
||||
mathEngine: 'MathJax' | 'KaTeX'
|
||||
messageStyle: 'plain' | 'bubble'
|
||||
// webdav 配置 host, user, pass, path
|
||||
webdavHost: string
|
||||
webdavUser: string
|
||||
@@ -44,7 +45,7 @@ const initialState: SettingsState = {
|
||||
theme: ThemeMode.auto,
|
||||
windowStyle: 'transparent',
|
||||
fontSize: 14,
|
||||
topicPosition: 'right',
|
||||
topicPosition: 'left',
|
||||
showTopicTime: false,
|
||||
pasteLongTextAsFile: false,
|
||||
clickAssistantToShowTopic: false,
|
||||
@@ -52,6 +53,7 @@ const initialState: SettingsState = {
|
||||
renderInputMessageAsMarkdown: true,
|
||||
codeShowLineNumbers: false,
|
||||
mathEngine: 'MathJax',
|
||||
messageStyle: 'plain',
|
||||
webdavHost: '',
|
||||
webdavUser: '',
|
||||
webdavPass: '',
|
||||
@@ -140,6 +142,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setMathEngine: (state, action: PayloadAction<'MathJax' | 'KaTeX'>) => {
|
||||
state.mathEngine = action.payload
|
||||
},
|
||||
setMessageStyle: (state, action: PayloadAction<'plain' | 'bubble'>) => {
|
||||
state.messageStyle = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -170,7 +175,8 @@ export const {
|
||||
setWebdavPass,
|
||||
setWebdavPath,
|
||||
setCodeShowLineNumbers,
|
||||
setMathEngine
|
||||
setMathEngine,
|
||||
setMessageStyle
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import db from '@renderer/databases'
|
||||
import { Message, Topic } from '@renderer/types'
|
||||
|
||||
export const exportMessageAsMarkdown = (message: Message) => {
|
||||
if (message.role === 'user') {
|
||||
return `### User\n\n${message.content}`
|
||||
}
|
||||
export const messageToMarkdown = (message: Message) => {
|
||||
const roleText = message.role === 'user' ? '🧑💻 User' : '🤖 Assistant'
|
||||
const titleSection = `### ${roleText}`
|
||||
const contentSection = message.content
|
||||
|
||||
return `### Assistant\n\n${message.content}`
|
||||
return [titleSection, '', contentSection].join('\n')
|
||||
}
|
||||
|
||||
export const exportMessagesAsMarkdown = (messages: Message[]) => {
|
||||
return messages.map((message) => exportMessageAsMarkdown(message)).join('\n\n---\n\n')
|
||||
export const messagesToMarkdown = (messages: Message[]) => {
|
||||
return messages.map((message) => messageToMarkdown(message)).join('\n\n---\n\n')
|
||||
}
|
||||
|
||||
export const topicToMarkdown = async (topic: Topic) => {
|
||||
const topicName = `# ${topic.name}`
|
||||
const topicMessages = await db.topics.get(topic.id)
|
||||
|
||||
if (topicMessages) {
|
||||
return topicName + '\n\n' + messagesToMarkdown(topicMessages.messages)
|
||||
}
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
export const exportTopicAsMarkdown = async (topic: Topic) => {
|
||||
const fileName = topic.name + '.md'
|
||||
const topicMessages = await db.topics.get(topic.id)
|
||||
if (topicMessages) {
|
||||
const title = '# ' + topic.name + `\n\n`
|
||||
const markdown = exportMessagesAsMarkdown(topicMessages.messages)
|
||||
window.api.file.save(fileName, title + markdown)
|
||||
}
|
||||
const markdown = await topicToMarkdown(topic)
|
||||
window.api.file.save(fileName, markdown)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import html2canvas from 'html2canvas'
|
||||
// @ts-ignore next-line`
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { classNames } from './style'
|
||||
|
||||
export const runAsyncFunction = async (fn: () => void) => {
|
||||
await fn()
|
||||
}
|
||||
@@ -330,10 +332,6 @@ export function formatFileSize(file: FileType) {
|
||||
return (size / 1024).toFixed(2) + ' KB'
|
||||
}
|
||||
|
||||
export function classNames(...classes: Array<string | boolean | undefined | null>) {
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export function sortByEnglishFirst(a: string, b: string) {
|
||||
const isAEnglish = /^[a-zA-Z]/.test(a)
|
||||
const isBEnglish = /^[a-zA-Z]/.test(b)
|
||||
@@ -341,3 +339,5 @@ export function sortByEnglishFirst(a: string, b: string) {
|
||||
if (!isAEnglish && isBEnglish) return 1
|
||||
return a.localeCompare(b)
|
||||
}
|
||||
|
||||
export { classNames }
|
||||
|
||||
45
src/renderer/src/utils/style.ts
Normal file
45
src/renderer/src/utils/style.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
type ClassValue = string | number | boolean | undefined | null | ClassDictionary | ClassArray
|
||||
|
||||
interface ClassDictionary {
|
||||
[id: string]: any
|
||||
}
|
||||
|
||||
interface ClassArray extends Array<ClassValue> {}
|
||||
|
||||
// Example:
|
||||
// classNames('foo', 'bar'); // => 'foo bar'
|
||||
// classNames('foo', { bar: true }); // => 'foo bar'
|
||||
// classNames({ foo: true, bar: false }); // => 'foo'
|
||||
// classNames(['foo', 'bar']); // => 'foo bar'
|
||||
// classNames('foo', null, 'bar'); // => 'foo bar'
|
||||
// classNames({ message: true, 'message-assistant': true }); // => 'message message-assistant'
|
||||
|
||||
/**
|
||||
* 生成 class 字符串
|
||||
* @param args
|
||||
* @returns
|
||||
*/
|
||||
export function classNames(...args: ClassValue[]): string {
|
||||
const classes: string[] = []
|
||||
|
||||
args.forEach((arg) => {
|
||||
if (!arg) return
|
||||
|
||||
if (typeof arg === 'string' || typeof arg === 'number') {
|
||||
classes.push(arg.toString())
|
||||
} else if (Array.isArray(arg)) {
|
||||
const inner = classNames(...arg)
|
||||
if (inner) {
|
||||
classes.push(inner)
|
||||
}
|
||||
} else if (typeof arg === 'object') {
|
||||
Object.entries(arg).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
classes.push(key)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return classes.filter(Boolean).join(' ')
|
||||
}
|
||||
144
yarn.lock
144
yarn.lock
@@ -1910,6 +1910,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/linkify-it@npm:^5":
|
||||
version: 5.0.0
|
||||
resolution: "@types/linkify-it@npm:5.0.0"
|
||||
checksum: 10c0/7bbbf45b9dde17bf3f184fee585aef0e7342f6954f0377a24e4ff42ab5a85d5b806aaa5c8d16e2faf2a6b87b2d94467a196b7d2b85c9c7de2f0eaac5487aaab8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/lodash@npm:^4.17.5":
|
||||
version: 4.17.7
|
||||
resolution: "@types/lodash@npm:4.17.7"
|
||||
@@ -1917,6 +1924,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/markdown-it@npm:^14":
|
||||
version: 14.1.2
|
||||
resolution: "@types/markdown-it@npm:14.1.2"
|
||||
dependencies:
|
||||
"@types/linkify-it": "npm:^5"
|
||||
"@types/mdurl": "npm:^2"
|
||||
checksum: 10c0/34f709f0476bd4e7b2ba7c3341072a6d532f1f4cb6f70aef371e403af8a08a7c372ba6907ac426bc618d356dab660c5b872791ff6c1ead80c483e0d639c6f127
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mathjax@npm:^0.0.40":
|
||||
version: 0.0.40
|
||||
resolution: "@types/mathjax@npm:0.0.40"
|
||||
@@ -1933,6 +1950,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/mdurl@npm:^2":
|
||||
version: 2.0.0
|
||||
resolution: "@types/mdurl@npm:2.0.0"
|
||||
checksum: 10c0/cde7bb571630ed1ceb3b92a28f7b59890bb38b8f34cd35326e2df43eebfc74985e6aa6fd4184e307393bad8a9e0783a519a3f9d13c8e03788c0f98e5ec869c5e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/minimatch@npm:*":
|
||||
version: 5.1.2
|
||||
resolution: "@types/minimatch@npm:5.1.2"
|
||||
@@ -1982,6 +2006,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:^22.7.5":
|
||||
version: 22.8.6
|
||||
resolution: "@types/node@npm:22.8.6"
|
||||
dependencies:
|
||||
undici-types: "npm:~6.19.8"
|
||||
checksum: 10c0/d3a11f2549234a91a4c5d0ff35ab4bdcb7ba34db4d3f1d189be39b8bd41c19aac98d117150a95a9c5a9d45b1014135477ea240b2b8317c86ae3d3cf1c3b3f8f4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/plist@npm:^3.0.1":
|
||||
version: 3.0.5
|
||||
resolution: "@types/plist@npm:3.0.5"
|
||||
@@ -2269,6 +2302,7 @@ __metadata:
|
||||
"@reduxjs/toolkit": "npm:^2.2.5"
|
||||
"@types/fs-extra": "npm:^11"
|
||||
"@types/lodash": "npm:^4.17.5"
|
||||
"@types/markdown-it": "npm:^14"
|
||||
"@types/node": "npm:^18.19.9"
|
||||
"@types/react": "npm:^18.2.48"
|
||||
"@types/react-dom": "npm:^18.2.18"
|
||||
@@ -2282,6 +2316,7 @@ __metadata:
|
||||
dayjs: "npm:^1.11.11"
|
||||
dexie: "npm:^4.0.8"
|
||||
dexie-react-hooks: "npm:^1.1.7"
|
||||
docx: "npm:^9.0.2"
|
||||
dotenv-cli: "npm:^7.4.2"
|
||||
electron: "npm:^28.3.3"
|
||||
electron-builder: "npm:^24.9.1"
|
||||
@@ -2305,6 +2340,7 @@ __metadata:
|
||||
i18next: "npm:^23.11.5"
|
||||
localforage: "npm:^1.10.0"
|
||||
lodash: "npm:^4.17.21"
|
||||
markdown-it: "npm:^14.1.0"
|
||||
mime: "npm:^4.0.4"
|
||||
officeparser: "npm:^4.1.1"
|
||||
openai: "npm:^4.52.1"
|
||||
@@ -4259,6 +4295,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"docx@npm:^9.0.2":
|
||||
version: 9.0.2
|
||||
resolution: "docx@npm:9.0.2"
|
||||
dependencies:
|
||||
"@types/node": "npm:^22.7.5"
|
||||
hash.js: "npm:^1.1.7"
|
||||
jszip: "npm:^3.10.1"
|
||||
nanoid: "npm:^5.0.4"
|
||||
xml: "npm:^1.0.1"
|
||||
xml-js: "npm:^1.6.8"
|
||||
checksum: 10c0/6065391bac9384084dd74f8ff1fb04ac781e8d10d0a0174bb62dc338e75130fac57f6413ff3e6b0f264d61850baddcccbfa7994be036c6fbc113127c4422d342
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dom-walk@npm:^0.1.0":
|
||||
version: 0.1.2
|
||||
resolution: "dom-walk@npm:0.1.2"
|
||||
@@ -5906,6 +5956,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hash.js@npm:^1.1.7":
|
||||
version: 1.1.7
|
||||
resolution: "hash.js@npm:1.1.7"
|
||||
dependencies:
|
||||
inherits: "npm:^2.0.3"
|
||||
minimalistic-assert: "npm:^1.0.1"
|
||||
checksum: 10c0/41ada59494eac5332cfc1ce6b7ebdd7b88a3864a6d6b08a3ea8ef261332ed60f37f10877e0c825aaa4bddebf164fbffa618286aeeec5296675e2671cbfa746c4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"hasha@npm:^2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "hasha@npm:2.2.0"
|
||||
@@ -7198,7 +7258,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jszip@npm:^3.1.0":
|
||||
"jszip@npm:^3.1.0, jszip@npm:^3.10.1":
|
||||
version: 3.10.1
|
||||
resolution: "jszip@npm:3.10.1"
|
||||
dependencies:
|
||||
@@ -7316,6 +7376,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"linkify-it@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "linkify-it@npm:5.0.0"
|
||||
dependencies:
|
||||
uc.micro: "npm:^2.0.0"
|
||||
checksum: 10c0/ff4abbcdfa2003472fc3eb4b8e60905ec97718e11e33cca52059919a4c80cc0e0c2a14d23e23d8c00e5402bc5a885cdba8ca053a11483ab3cc8b3c7a52f88e2d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"load-bmfont@npm:^1.3.1, load-bmfont@npm:^1.4.0":
|
||||
version: 1.4.2
|
||||
resolution: "load-bmfont@npm:1.4.2"
|
||||
@@ -7523,6 +7592,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"markdown-it@npm:^14.1.0":
|
||||
version: 14.1.0
|
||||
resolution: "markdown-it@npm:14.1.0"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
entities: "npm:^4.4.0"
|
||||
linkify-it: "npm:^5.0.0"
|
||||
mdurl: "npm:^2.0.0"
|
||||
punycode.js: "npm:^2.3.1"
|
||||
uc.micro: "npm:^2.1.0"
|
||||
bin:
|
||||
markdown-it: bin/markdown-it.mjs
|
||||
checksum: 10c0/9a6bb444181d2db7016a4173ae56a95a62c84d4cbfb6916a399b11d3e6581bf1cc2e4e1d07a2f022ae72c25f56db90fbe1e529fca16fbf9541659dc53480d4b4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"markdown-table@npm:^3.0.0":
|
||||
version: 3.0.3
|
||||
resolution: "markdown-table@npm:3.0.3"
|
||||
@@ -7793,6 +7878,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mdurl@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "mdurl@npm:2.0.0"
|
||||
checksum: 10c0/633db522272f75ce4788440669137c77540d74a83e9015666a9557a152c02e245b192edc20bc90ae953bbab727503994a53b236b4d9c99bdaee594d0e7dd2ce0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"memoize-one@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "memoize-one@npm:6.0.0"
|
||||
@@ -8248,6 +8340,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimalistic-assert@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "minimalistic-assert@npm:1.0.1"
|
||||
checksum: 10c0/96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:9.0.3":
|
||||
version: 9.0.3
|
||||
resolution: "minimatch@npm:9.0.3"
|
||||
@@ -8463,6 +8562,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nanoid@npm:^5.0.4":
|
||||
version: 5.0.8
|
||||
resolution: "nanoid@npm:5.0.8"
|
||||
bin:
|
||||
nanoid: bin/nanoid.js
|
||||
checksum: 10c0/0281d3cc0f3d03fb3010b479f28e8ddedfafb9b5d60e54315589ef0a54a0e9cfcb0bf382dd3b620fdad6b72b8c1f5b89a15d55c6b6a9134b58b16eb230b3a3d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"napi-build-utils@npm:^1.0.1":
|
||||
version: 1.0.2
|
||||
resolution: "napi-build-utils@npm:1.0.2"
|
||||
@@ -9436,6 +9544,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"punycode.js@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "punycode.js@npm:2.3.1"
|
||||
checksum: 10c0/1d12c1c0e06127fa5db56bd7fdf698daf9a78104456a6b67326877afc21feaa821257b171539caedd2f0524027fa38e67b13dd094159c8d70b6d26d2bea4dfdb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "punycode@npm:2.3.1"
|
||||
@@ -12036,6 +12151,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "uc.micro@npm:2.1.0"
|
||||
checksum: 10c0/8862eddb412dda76f15db8ad1c640ccc2f47cdf8252a4a30be908d535602c8d33f9855dfcccb8b8837855c1ce1eaa563f7fa7ebe3c98fd0794351aab9b9c55fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unbox-primitive@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "unbox-primitive@npm:1.0.2"
|
||||
@@ -12065,7 +12187,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici-types@npm:~6.19.2":
|
||||
"undici-types@npm:~6.19.2, undici-types@npm:~6.19.8":
|
||||
version: 6.19.8
|
||||
resolution: "undici-types@npm:6.19.8"
|
||||
checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344
|
||||
@@ -12760,6 +12882,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xml-js@npm:^1.6.8":
|
||||
version: 1.6.11
|
||||
resolution: "xml-js@npm:1.6.11"
|
||||
dependencies:
|
||||
sax: "npm:^1.2.4"
|
||||
bin:
|
||||
xml-js: ./bin/cli.js
|
||||
checksum: 10c0/c83631057f10bf90ea785cee434a8a1a0030c7314fe737ad9bf568a281083b565b28b14c9e9ba82f11fc9dc582a3a907904956af60beb725be1c9ad4b030bc5a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xml-name-validator@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "xml-name-validator@npm:5.0.0"
|
||||
@@ -12784,6 +12917,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xml@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "xml@npm:1.0.1"
|
||||
checksum: 10c0/04bcc9b8b5e7b49392072fbd9c6b0f0958bd8e8f8606fee460318e43991349a68cbc5384038d179ff15aef7d222285f69ca0f067f53d071084eb14c7fdb30411
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"xmlbuilder@npm:>=11.0.1, xmlbuilder@npm:^15.1.1":
|
||||
version: 15.1.1
|
||||
resolution: "xmlbuilder@npm:15.1.1"
|
||||
|
||||
Reference in New Issue
Block a user