feat: add mini window
This commit is contained in:
parent
d9bb552f3f
commit
a7d9700f06
@ -50,7 +50,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js', 'chunk-AFE5XGNG.js', 'chunk-QIJABHCK.js']
|
||||
exclude: ['chunk-RK3FTE5R.js']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -85,6 +85,7 @@
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
"@types/adm-zip": "^0",
|
||||
"@types/ffi-napi": "^4",
|
||||
"@types/fs-extra": "^11",
|
||||
"@types/lodash": "^4.17.5",
|
||||
"@types/markdown-it": "^14",
|
||||
@ -92,9 +93,11 @@
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||
"@types/ref-napi": "^3",
|
||||
"@types/tinycolor2": "^1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"antd": "^5.22.5",
|
||||
"applescript": "^1.0.0",
|
||||
"axios": "^1.7.3",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"dayjs": "^1.11.11",
|
||||
|
||||
117
resources/textMonitor.swift
Normal file
117
resources/textMonitor.swift
Normal file
@ -0,0 +1,117 @@
|
||||
import Cocoa
|
||||
import Foundation
|
||||
|
||||
class TextSelectionObserver: NSObject {
|
||||
let workspace = NSWorkspace.shared
|
||||
var lastSelectedText: String?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
// 注册通知观察者
|
||||
let observer = NSWorkspace.shared.notificationCenter
|
||||
observer.addObserver(
|
||||
self,
|
||||
selector: #selector(handleSelectionChange),
|
||||
name: NSWorkspace.didActivateApplicationNotification,
|
||||
object: nil
|
||||
)
|
||||
|
||||
// 监听选择变化通知
|
||||
var axObserver: AXObserver?
|
||||
let error = AXObserverCreate(getpid(), { observer, element, notification, userData in
|
||||
let selfPointer = userData!.load(as: TextSelectionObserver.self)
|
||||
selfPointer.checkSelectedText()
|
||||
}, &axObserver)
|
||||
|
||||
if error == .success, let axObserver = axObserver {
|
||||
CFRunLoopAddSource(
|
||||
RunLoop.main.getCFRunLoop(),
|
||||
AXObserverGetRunLoopSource(axObserver),
|
||||
.defaultMode
|
||||
)
|
||||
|
||||
// 当前活动应用添加监听
|
||||
updateActiveAppObserver(axObserver)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleSelectionChange(_ notification: Notification) {
|
||||
// 应用切换时更新监听
|
||||
var axObserver: AXObserver?
|
||||
let error = AXObserverCreate(getpid(), { _, _, _, _ in }, &axObserver)
|
||||
if error == .success, let axObserver = axObserver {
|
||||
updateActiveAppObserver(axObserver)
|
||||
}
|
||||
}
|
||||
|
||||
func updateActiveAppObserver(_ axObserver: AXObserver) {
|
||||
guard let app = workspace.frontmostApplication else { return }
|
||||
let pid = app.processIdentifier
|
||||
let element = AXUIElementCreateApplication(pid)
|
||||
|
||||
// 添加选择变化通知监听
|
||||
AXObserverAddNotification(
|
||||
axObserver,
|
||||
element,
|
||||
kAXSelectedTextChangedNotification as CFString,
|
||||
UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())
|
||||
)
|
||||
}
|
||||
|
||||
func checkSelectedText() {
|
||||
if let text = getSelectedText() {
|
||||
if text.count > 0 && text != lastSelectedText {
|
||||
print(text)
|
||||
fflush(stdout)
|
||||
lastSelectedText = text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getSelectedText() -> String? {
|
||||
guard let app = NSWorkspace.shared.frontmostApplication else { return nil }
|
||||
let pid = app.processIdentifier
|
||||
|
||||
let axApp = AXUIElementCreateApplication(pid)
|
||||
var focusedElement: AnyObject?
|
||||
|
||||
// Get focused element
|
||||
let result = AXUIElementCopyAttributeValue(axApp, kAXFocusedUIElementAttribute as CFString, &focusedElement)
|
||||
guard result == .success else { return nil }
|
||||
|
||||
// Try different approaches to get selected text
|
||||
var selectedText: AnyObject?
|
||||
|
||||
// First try: Direct selected text
|
||||
var textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
|
||||
|
||||
// Second try: Selected text in text area
|
||||
if textResult != .success {
|
||||
var selectedTextRange: AnyObject?
|
||||
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXSelectedTextRangeAttribute as CFString, &selectedTextRange)
|
||||
if textResult == .success {
|
||||
textResult = AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXValueAttribute as CFString, &selectedText)
|
||||
}
|
||||
}
|
||||
|
||||
// Third try: Get selected text from parent element
|
||||
if textResult != .success {
|
||||
var parent: AnyObject?
|
||||
if AXUIElementCopyAttributeValue(focusedElement as! AXUIElement, kAXParentAttribute as CFString, &parent) == .success {
|
||||
textResult = AXUIElementCopyAttributeValue(parent as! AXUIElement, kAXSelectedTextAttribute as CFString, &selectedText)
|
||||
}
|
||||
}
|
||||
|
||||
guard textResult == .success, let text = selectedText as? String else { return nil }
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
let observer = TextSelectionObserver()
|
||||
|
||||
signal(SIGINT) { _ in
|
||||
exit(0)
|
||||
}
|
||||
|
||||
RunLoop.main.run()
|
||||
@ -52,6 +52,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
configManager.setTray(isActive)
|
||||
})
|
||||
|
||||
ipcMain.handle('config:set', (_, key: string, value: any) => {
|
||||
configManager.set(key, value)
|
||||
})
|
||||
|
||||
ipcMain.handle('config:get', (_, key: string) => {
|
||||
return configManager.get(key)
|
||||
})
|
||||
|
||||
// theme
|
||||
ipcMain.handle('app:set-theme', (_, theme: ThemeMode) => {
|
||||
configManager.setTheme(theme)
|
||||
|
||||
118
src/main/services/ClipboardMonitor.ts
Normal file
118
src/main/services/ClipboardMonitor.ts
Normal file
@ -0,0 +1,118 @@
|
||||
import { debounce, getResourcePath } from '@main/utils'
|
||||
import { exec } from 'child_process'
|
||||
import { screen } from 'electron'
|
||||
import path from 'path'
|
||||
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
export default class ClipboardMonitor {
|
||||
private platform: string
|
||||
private lastText: string
|
||||
private user32: any
|
||||
private observer: any
|
||||
public onTextSelected: (text: string) => void
|
||||
|
||||
constructor() {
|
||||
this.platform = process.platform
|
||||
this.lastText = ''
|
||||
this.onTextSelected = debounce((text: string) => this.handleTextSelected(text), 550)
|
||||
|
||||
if (this.platform === 'win32') {
|
||||
this.setupWindows()
|
||||
} else if (this.platform === 'darwin') {
|
||||
this.setupMacOS()
|
||||
}
|
||||
}
|
||||
|
||||
setupMacOS() {
|
||||
// 使用 Swift 脚本来监听文本选择
|
||||
const scriptPath = path.join(getResourcePath(), 'textMonitor.swift')
|
||||
|
||||
// 启动 Swift 进程来监听文本选择
|
||||
const process = exec(`swift ${scriptPath}`)
|
||||
|
||||
process?.stdout?.on('data', (data: string) => {
|
||||
console.log('[ClipboardMonitor] MacOS data:', data)
|
||||
const text = data.toString().trim()
|
||||
if (text && text !== this.lastText) {
|
||||
this.lastText = text
|
||||
this.onTextSelected(text)
|
||||
}
|
||||
})
|
||||
|
||||
process.on('error', (error) => {
|
||||
console.error('[ClipboardMonitor] MacOS error:', error)
|
||||
})
|
||||
}
|
||||
|
||||
setupWindows() {
|
||||
// 使用 Windows API 监听文本选择事件
|
||||
const ffi = require('ffi-napi')
|
||||
const ref = require('ref-napi')
|
||||
|
||||
this.user32 = new ffi.Library('user32', {
|
||||
SetWinEventHook: ['pointer', ['uint32', 'uint32', 'pointer', 'pointer', 'uint32', 'uint32', 'uint32']],
|
||||
UnhookWinEvent: ['bool', ['pointer']]
|
||||
})
|
||||
|
||||
// 定义事件常量
|
||||
const EVENT_OBJECT_SELECTION = 0x8006
|
||||
const WINEVENT_OUTOFCONTEXT = 0x0000
|
||||
const WINEVENT_SKIPOWNTHREAD = 0x0001
|
||||
const WINEVENT_SKIPOWNPROCESS = 0x0002
|
||||
|
||||
// 创建回调函数
|
||||
const callback = ffi.Callback('void', ['pointer', 'uint32', 'pointer', 'long', 'long', 'uint32', 'uint32'], () => {
|
||||
this.getSelectedText()
|
||||
})
|
||||
|
||||
// 设置事件钩子
|
||||
this.observer = this.user32.SetWinEventHook(
|
||||
EVENT_OBJECT_SELECTION,
|
||||
EVENT_OBJECT_SELECTION,
|
||||
ref.NULL,
|
||||
callback,
|
||||
0,
|
||||
0,
|
||||
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNTHREAD | WINEVENT_SKIPOWNPROCESS
|
||||
)
|
||||
}
|
||||
|
||||
getSelectedText() {
|
||||
// Get selected text
|
||||
if (this.platform === 'win32') {
|
||||
const ref = require('ref-napi')
|
||||
if (this.user32.OpenClipboard(ref.NULL)) {
|
||||
// Get clipboard content
|
||||
const text = this.user32.GetClipboardData(1) // CF_TEXT = 1
|
||||
this.user32.CloseClipboard()
|
||||
|
||||
if (text && text !== this.lastText) {
|
||||
this.lastText = text
|
||||
this.onTextSelected(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleTextSelected(text: string) {
|
||||
if (!text) return
|
||||
|
||||
console.debug('[ClipboardMonitor] handleTextSelected', text)
|
||||
|
||||
windowService.setLastSelectedText(text)
|
||||
|
||||
const mousePosition = screen.getCursorScreenPoint()
|
||||
|
||||
windowService.showSelectionMenu({
|
||||
x: mousePosition.x,
|
||||
y: mousePosition.y + 10
|
||||
})
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.platform === 'win32' && this.observer) {
|
||||
this.user32.UnhookWinEvent(this.observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -30,7 +30,7 @@ export class ConfigManager {
|
||||
this.store.set('theme', theme)
|
||||
}
|
||||
|
||||
isTray(): boolean {
|
||||
getTray(): boolean {
|
||||
return !!this.store.get('tray', true)
|
||||
}
|
||||
|
||||
@ -83,6 +83,22 @@ export class ConfigManager {
|
||||
)
|
||||
this.notifySubscribers('shortcuts', shortcuts)
|
||||
}
|
||||
|
||||
getClickTrayToShowQuickAssistant(): boolean {
|
||||
return this.store.get('clickTrayToShowQuickAssistant', false) as boolean
|
||||
}
|
||||
|
||||
setClickTrayToShowQuickAssistant(value: boolean) {
|
||||
this.store.set('clickTrayToShowQuickAssistant', value)
|
||||
}
|
||||
|
||||
set(key: string, value: any) {
|
||||
this.store.set(key, value)
|
||||
}
|
||||
|
||||
get(key: string) {
|
||||
return this.store.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
export const configManager = new ConfigManager()
|
||||
|
||||
@ -3,8 +3,10 @@ import { BrowserWindow, globalShortcut } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { configManager } from './ConfigManager'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
let showAppAccelerator: string | null = null
|
||||
let showMiniWindowAccelerator: string | null = null
|
||||
|
||||
function getShortcutHandler(shortcut: Shortcut) {
|
||||
switch (shortcut.key) {
|
||||
@ -26,6 +28,10 @@ function getShortcutHandler(shortcut: Shortcut) {
|
||||
window.focus()
|
||||
}
|
||||
}
|
||||
case 'mini_window':
|
||||
return () => {
|
||||
windowService.toggleMiniWindow()
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@ -73,6 +79,10 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
showAppAccelerator = accelerator
|
||||
}
|
||||
|
||||
if (shortcut.key === 'mini_window') {
|
||||
showMiniWindowAccelerator = accelerator
|
||||
}
|
||||
|
||||
if (shortcut.key.includes('zoom')) {
|
||||
switch (shortcut.key) {
|
||||
case 'zoom_in':
|
||||
@ -90,7 +100,7 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
}
|
||||
|
||||
if (shortcut.enabled) {
|
||||
globalShortcut.register(accelerator, () => handler(window))
|
||||
globalShortcut.register(formatShortcutKey(shortcut.shortcut), () => handler(window))
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
|
||||
@ -108,6 +118,11 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
|
||||
handler && globalShortcut.register(showAppAccelerator, () => handler(window))
|
||||
}
|
||||
|
||||
if (showMiniWindowAccelerator) {
|
||||
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
|
||||
handler && globalShortcut.register(showMiniWindowAccelerator, () => handler(window))
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[ShortcutService] Failed to unregister shortcuts')
|
||||
}
|
||||
@ -124,6 +139,7 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
export function unregisterAllShortcuts() {
|
||||
try {
|
||||
showAppAccelerator = null
|
||||
showMiniWindowAccelerator = null
|
||||
globalShortcut.unregisterAll()
|
||||
} catch (error) {
|
||||
Logger.error('[ShortcutService] Failed to unregister all shortcuts')
|
||||
|
||||
@ -17,6 +17,8 @@ export class TrayService {
|
||||
}
|
||||
|
||||
private createTray() {
|
||||
this.destroyTray()
|
||||
|
||||
const iconPath = isMac ? (nativeTheme.shouldUseDarkColors ? iconLight : iconDark) : icon
|
||||
const tray = new Tray(iconPath)
|
||||
|
||||
@ -43,6 +45,10 @@ export class TrayService {
|
||||
label: trayLocale.show_window,
|
||||
click: () => windowService.showMainWindow()
|
||||
},
|
||||
{
|
||||
label: trayLocale.show_mini_window,
|
||||
click: () => windowService.showMiniWindow()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: trayLocale.quit,
|
||||
@ -61,12 +67,17 @@ export class TrayService {
|
||||
})
|
||||
|
||||
this.tray.on('click', () => {
|
||||
if (configManager.getClickTrayToShowQuickAssistant()) {
|
||||
windowService.showMiniWindow()
|
||||
} else {
|
||||
windowService.showMainWindow()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private updateTray() {
|
||||
if (configManager.isTray()) {
|
||||
const showTray = configManager.getTray()
|
||||
if (showTray) {
|
||||
this.createTray()
|
||||
} else {
|
||||
this.destroyTray()
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { isLinux, isWin } from '@main/constant'
|
||||
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import path, { join } from 'path'
|
||||
@ -13,8 +13,11 @@ import { configManager } from './ConfigManager'
|
||||
export class WindowService {
|
||||
private static instance: WindowService | null = null
|
||||
private mainWindow: BrowserWindow | null = null
|
||||
private miniWindow: BrowserWindow | null = null
|
||||
private isQuitting: boolean = false
|
||||
private wasFullScreen: boolean = false
|
||||
private selectionMenuWindow: BrowserWindow | null = null
|
||||
private lastSelectedText: string = ''
|
||||
|
||||
public static getInstance(): WindowService {
|
||||
if (!WindowService.instance) {
|
||||
@ -63,6 +66,9 @@ export class WindowService {
|
||||
})
|
||||
|
||||
this.setupMainWindow(this.mainWindow, mainWindowState)
|
||||
|
||||
setTimeout(() => this.showMiniWindow(), 5000)
|
||||
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
@ -201,7 +207,7 @@ export class WindowService {
|
||||
})
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
const notInTray = !configManager.isTray()
|
||||
const notInTray = !configManager.getTray()
|
||||
|
||||
// Windows and Linux
|
||||
if ((isWin || isLinux) && notInTray) {
|
||||
@ -233,6 +239,154 @@ export class WindowService {
|
||||
this.createMainWindow()
|
||||
}
|
||||
}
|
||||
|
||||
public showMiniWindow() {
|
||||
if (this.selectionMenuWindow) {
|
||||
this.selectionMenuWindow.hide()
|
||||
}
|
||||
|
||||
if (this.miniWindow && !this.miniWindow.isDestroyed()) {
|
||||
if (this.miniWindow.isMinimized()) {
|
||||
this.miniWindow.restore()
|
||||
}
|
||||
this.miniWindow.show()
|
||||
this.miniWindow.center()
|
||||
this.miniWindow.focus()
|
||||
return
|
||||
}
|
||||
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
this.miniWindow = new BrowserWindow({
|
||||
width: 500,
|
||||
height: 520,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'under-window',
|
||||
visualEffectState: 'followWindow',
|
||||
center: true,
|
||||
frame: false,
|
||||
alwaysOnTop: true,
|
||||
resizable: false,
|
||||
useContentSize: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
webviewTag: true
|
||||
}
|
||||
})
|
||||
|
||||
this.miniWindow.on('blur', () => {
|
||||
this.miniWindow?.hide()
|
||||
})
|
||||
|
||||
this.miniWindow.on('close', (event) => {
|
||||
if (this.isQuitting) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
this.miniWindow?.hide()
|
||||
})
|
||||
|
||||
this.miniWindow.on('closed', () => {
|
||||
this.miniWindow = null
|
||||
})
|
||||
|
||||
this.miniWindow.on('hide', () => {
|
||||
this.miniWindow?.webContents.send('hide-mini-window')
|
||||
})
|
||||
|
||||
this.miniWindow.on('show', () => {
|
||||
this.miniWindow?.webContents.send('show-mini-window')
|
||||
})
|
||||
|
||||
ipcMain.on('miniwindow-reload', () => {
|
||||
this.miniWindow?.reload()
|
||||
})
|
||||
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
this.miniWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '#/mini')
|
||||
} else {
|
||||
this.miniWindow.loadFile(join(__dirname, '../renderer/index.html') + '#/mini')
|
||||
}
|
||||
}
|
||||
|
||||
public toggleMiniWindow() {
|
||||
if (this.miniWindow) {
|
||||
this.miniWindow.isVisible() ? this.miniWindow.hide() : this.miniWindow.show()
|
||||
}
|
||||
}
|
||||
|
||||
public showSelectionMenu(bounds: { x: number; y: number }) {
|
||||
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
|
||||
this.selectionMenuWindow.setPosition(bounds.x, bounds.y)
|
||||
this.selectionMenuWindow.show()
|
||||
return
|
||||
}
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
this.selectionMenuWindow = new BrowserWindow({
|
||||
width: 280,
|
||||
height: 40,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
alwaysOnTop: false,
|
||||
skipTaskbar: true,
|
||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||
resizable: false,
|
||||
vibrancy: 'popover',
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false
|
||||
}
|
||||
})
|
||||
|
||||
// 点击其他地方时隐藏窗口
|
||||
this.selectionMenuWindow.on('blur', () => {
|
||||
this.selectionMenuWindow?.hide()
|
||||
this.miniWindow?.webContents.send('selection-action', {
|
||||
action: 'home',
|
||||
selectedText: this.lastSelectedText
|
||||
})
|
||||
})
|
||||
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
this.selectionMenuWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/src/windows/menu/menu.html')
|
||||
} else {
|
||||
this.selectionMenuWindow.loadFile(join(__dirname, '../renderer/src/windows/menu/menu.html'))
|
||||
}
|
||||
|
||||
this.setupSelectionMenuEvents()
|
||||
}
|
||||
|
||||
private setupSelectionMenuEvents() {
|
||||
if (!this.selectionMenuWindow) return
|
||||
|
||||
ipcMain.removeHandler('selection-menu:action')
|
||||
ipcMain.handle('selection-menu:action', (_, action) => {
|
||||
this.selectionMenuWindow?.hide()
|
||||
this.showMiniWindow()
|
||||
setTimeout(() => {
|
||||
this.miniWindow?.webContents.send('selection-action', {
|
||||
action,
|
||||
selectedText: this.lastSelectedText
|
||||
})
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
public setLastSelectedText(text: string) {
|
||||
this.lastSelectedText = text
|
||||
}
|
||||
}
|
||||
|
||||
export const windowService = WindowService.getInstance()
|
||||
|
||||
@ -22,3 +22,15 @@ export function getInstanceName(baseURL: string) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function debounce(func: (...args: any[]) => void, wait: number, immediate: boolean = false) {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
return function (...args: any[]) {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
if (immediate) {
|
||||
func(...args)
|
||||
} else {
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
src/preload/index.d.ts
vendored
7
src/preload/index.d.ts
vendored
@ -89,6 +89,13 @@ declare global {
|
||||
listFiles: (apiKey: string) => Promise<ListFilesResponse>
|
||||
deleteFile: (apiKey: string, fileId: string) => Promise<void>
|
||||
}
|
||||
selectionMenu: {
|
||||
action: (action: string) => Promise<void>
|
||||
}
|
||||
config: {
|
||||
set: (key: string, value: any) => Promise<void>
|
||||
get: (key: string) => Promise<any>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -82,6 +82,13 @@ const api = {
|
||||
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:retrieve-file', file, apiKey),
|
||||
listFiles: (apiKey: string) => ipcRenderer.invoke('gemini:list-files', apiKey),
|
||||
deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke('gemini:delete-file', apiKey, fileId)
|
||||
},
|
||||
selectionMenu: {
|
||||
action: (action: string) => ipcRenderer.invoke('selection-menu:action', action)
|
||||
},
|
||||
config: {
|
||||
set: (key: string, value: any) => ipcRenderer.invoke('config:set', key, value),
|
||||
get: (key: string) => ipcRenderer.invoke('config:get', key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -17,10 +17,10 @@
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#spinner img {
|
||||
@ -35,6 +35,7 @@
|
||||
<div id="spinner">
|
||||
<img src="/src/assets/images/logo.png" />
|
||||
</div>
|
||||
<script type="module" src="/src/init.ts"></script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
--color-background: var(--color-black);
|
||||
--color-background-soft: var(--color-black-soft);
|
||||
--color-background-mute: var(--color-black-mute);
|
||||
--color-background-opacity: rgba(34, 34, 34, 0.7);
|
||||
|
||||
--color-primary: #00b96b;
|
||||
--color-primary-soft: #00b96b99;
|
||||
@ -87,6 +88,7 @@ body[theme-mode='light'] {
|
||||
--color-background: var(--color-white);
|
||||
--color-background-soft: var(--color-white-soft);
|
||||
--color-background-mute: var(--color-white-mute);
|
||||
--color-background-opacity: rgba(255, 255, 255, 0.7);
|
||||
|
||||
--color-primary: #00b96b;
|
||||
--color-primary-soft: #00b96b99;
|
||||
|
||||
@ -13,7 +13,11 @@ const ThemeContext = createContext<ThemeContextType>({
|
||||
toggleTheme: () => {}
|
||||
})
|
||||
|
||||
export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
interface ThemeProviderProps extends PropsWithChildren {
|
||||
defaultTheme?: ThemeMode
|
||||
}
|
||||
|
||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultTheme }) => {
|
||||
const { theme, setTheme } = useSettings()
|
||||
const [_theme, _setTheme] = useState(theme)
|
||||
|
||||
@ -22,7 +26,7 @@ export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
}
|
||||
|
||||
useEffect((): any => {
|
||||
if (theme === ThemeMode.auto) {
|
||||
if (theme === ThemeMode.auto || defaultTheme === ThemeMode.auto) {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
_setTheme(mediaQuery.matches ? ThemeMode.dark : ThemeMode.light)
|
||||
const handleChange = (e: MediaQueryListEvent) => _setTheme(e.matches ? ThemeMode.dark : ThemeMode.light)
|
||||
@ -31,7 +35,7 @@ export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
} else {
|
||||
_setTheme(theme)
|
||||
}
|
||||
}, [theme])
|
||||
}, [defaultTheme, theme])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('theme-mode', _theme)
|
||||
|
||||
@ -9,7 +9,10 @@ export default function useUpdateHandler() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electron) return
|
||||
|
||||
const ipcRenderer = window.electron.ipcRenderer
|
||||
|
||||
const removers = [
|
||||
ipcRenderer.on('update-not-available', () => {
|
||||
dispatch(setUpdateState({ checking: false }))
|
||||
|
||||
@ -385,6 +385,10 @@
|
||||
"webdav.syncError": "Backup Error",
|
||||
"webdav.lastSync": "Last Backup"
|
||||
},
|
||||
"quickAssistant": {
|
||||
"title": "Quick Assistant",
|
||||
"click_tray_to_show": "Click the system tray icon to open"
|
||||
},
|
||||
"display.title": "Display Settings",
|
||||
"font_size.title": "Message font size",
|
||||
"general": "General Settings",
|
||||
@ -519,7 +523,8 @@
|
||||
"toggle_show_assistants": "Toggle Assistants",
|
||||
"toggle_show_topics": "Toggle Topics",
|
||||
"copy_last_message": "Copy Last Message",
|
||||
"search_message": "Search Message"
|
||||
"search_message": "Search Message",
|
||||
"mini_window": "Quick Assistant"
|
||||
},
|
||||
"theme.auto": "Auto",
|
||||
"theme.dark": "Dark",
|
||||
@ -552,7 +557,8 @@
|
||||
},
|
||||
"tray": {
|
||||
"quit": "Quit",
|
||||
"show_window": "Show Window"
|
||||
"show_window": "Show Window",
|
||||
"show_mini_window": "Quick Assistant"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "Knowledge Graph",
|
||||
@ -634,7 +640,31 @@
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"summarize": "You are an assistant who is good at conversation. You need to summarize the user's conversation into a title of 10 characters or less, ensuring it matches the user's primary language without using punctuation or other special symbols."
|
||||
"title": "You are an assistant who is good at conversation. You need to summarize the user's conversation into a title of 10 characters or less, ensuring it matches the user's primary language without using punctuation or other special symbols.",
|
||||
"explanation": "Explain this concept to me",
|
||||
"summarize": "Summarize this text"
|
||||
},
|
||||
"miniwindow": {
|
||||
"feature": {
|
||||
"chat": "Answer this question",
|
||||
"translate": "Text translation",
|
||||
"summary": "Content summary",
|
||||
"explanation": "Explanation"
|
||||
},
|
||||
"clipboard": {
|
||||
"empty": "Clipboard is empty"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": {
|
||||
"title": "What do you want to do with this text?",
|
||||
"empty": "Ask {{model}} for help..."
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"esc": "Press ESC {{action}}",
|
||||
"esc_close": "close the window",
|
||||
"esc_back": "back"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -383,6 +383,10 @@
|
||||
"webdav.syncError": "バックアップエラー",
|
||||
"webdav.lastSync": "最終同期"
|
||||
},
|
||||
"quickAssistant": {
|
||||
"title": "クイックアシスタント",
|
||||
"click_tray_to_show": "システムトレイアイコンをクリックして開く"
|
||||
},
|
||||
"display.title": "表示設定",
|
||||
"font_size.title": "メッセージのフォントサイズ",
|
||||
"general": "一般設定",
|
||||
@ -504,7 +508,8 @@
|
||||
"toggle_show_assistants": "アシスタントの表示を切り替え",
|
||||
"toggle_show_topics": "トピックの表示を切り替え",
|
||||
"copy_last_message": "最後のメッセージをコピー",
|
||||
"search_message": "メッセージを検索"
|
||||
"search_message": "メッセージを検索",
|
||||
"mini_window": "クイックアシスタント"
|
||||
},
|
||||
"theme.auto": "自動",
|
||||
"theme.dark": "ダークテーマ",
|
||||
@ -537,7 +542,8 @@
|
||||
},
|
||||
"tray": {
|
||||
"quit": "終了",
|
||||
"show_window": "ウィンドウを表示"
|
||||
"show_window": "ウィンドウを表示",
|
||||
"show_mini_window": "クイックアシスタント"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "ナレッジグラフ",
|
||||
@ -619,7 +625,31 @@
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"summarize": "あなたは会話を得意とするアシスタントです。ユーザーの会話を10文字以内のタイトルに要約し、ユーザーの主言語と一致していることを確認してください。句読点や特殊記号は使用しないでください。"
|
||||
"title": "あなたは会話を得意とするアシスタントです。ユーザーの会話を10文字以内のタイトルに要約し、ユーザーの主言語と一致していることを確認してください。句読点や特殊記号は使用しないでください。",
|
||||
"explanation": "この概念を説明してください",
|
||||
"summarize": "このテキストを要約してください"
|
||||
},
|
||||
"miniwindow": {
|
||||
"feature": {
|
||||
"chat": "この質問に回答",
|
||||
"translate": "テキスト翻訳",
|
||||
"summary": "内容要約",
|
||||
"explanation": "説明"
|
||||
},
|
||||
"clipboard": {
|
||||
"empty": "クリップボードが空です"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": {
|
||||
"title": "下のテキストに対して何をしますか?",
|
||||
"empty": "{{model}} に質問してください..."
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"esc": "ESC キーを押して{{action}}",
|
||||
"esc_close": "ウィンドウを閉じる",
|
||||
"esc_back": "戻る"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -385,6 +385,10 @@
|
||||
"webdav.syncError": "Ошибка резервного копирования",
|
||||
"webdav.lastSync": "Последняя синхронизация"
|
||||
},
|
||||
"quickAssistant": {
|
||||
"title": "Быстрый помощник",
|
||||
"click_tray_to_show": "Нажмите на иконку системного трея для открытия"
|
||||
},
|
||||
"display.title": "Настройки отображения",
|
||||
"font_size.title": "Размер шрифта сообщений",
|
||||
"general": "Общие настройки",
|
||||
@ -518,7 +522,8 @@
|
||||
"toggle_show_assistants": "Переключить отображение ассистентов",
|
||||
"toggle_show_topics": "Переключить отображение топиков",
|
||||
"copy_last_message": "Копировать последнее сообщение",
|
||||
"search_message": "Поиск сообщения"
|
||||
"search_message": "Поиск сообщения",
|
||||
"mini_window": "Быстрый помощник"
|
||||
},
|
||||
"theme.auto": "Автоматически",
|
||||
"theme.dark": "Темная",
|
||||
@ -551,7 +556,8 @@
|
||||
},
|
||||
"tray": {
|
||||
"quit": "Выйти",
|
||||
"show_window": "Показать окно"
|
||||
"show_window": "Показать окно",
|
||||
"show_mini_window": "Быстрый помощник"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "Граф знаний",
|
||||
@ -633,7 +639,31 @@
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"summarize": "Вы - эксперт в общении, который суммирует разговоры пользователя в 10-символьном заголовке, совпадающем с языком пользователя, без использования знаков препинания и других специальных символов"
|
||||
"title": "Вы - эксперт в общении, который суммирует разговоры пользователя в 10-символьном заголовке, совпадающем с языком пользователя, без использования знаков препинания и других специальных символов",
|
||||
"explanation": "Объясните мне этот концепт",
|
||||
"summarize": "Суммируйте этот текст"
|
||||
},
|
||||
"miniwindow": {
|
||||
"feature": {
|
||||
"chat": "Ответить на этот вопрос",
|
||||
"translate": "Текст перевод",
|
||||
"summary": "Содержание",
|
||||
"explanation": "Объяснение"
|
||||
},
|
||||
"clipboard": {
|
||||
"empty": "Буфер обмена пуст"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": {
|
||||
"title": "Что вы хотите сделать с этим текстом?",
|
||||
"empty": "Задайте вопрос {{model}}..."
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"esc": "Нажмите ESC {{action}}",
|
||||
"esc_close": "закрытия окна",
|
||||
"esc_back": "возвращения"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -386,6 +386,10 @@
|
||||
"webdav.syncError": "备份错误",
|
||||
"webdav.lastSync": "上次备份时间"
|
||||
},
|
||||
"quickAssistant": {
|
||||
"title": "快捷助手",
|
||||
"click_tray_to_show": "点击系统托盘图标打开"
|
||||
},
|
||||
"display.title": "显示设置",
|
||||
"font_size.title": "消息字体大小",
|
||||
"general": "常规设置",
|
||||
@ -507,7 +511,8 @@
|
||||
"toggle_show_assistants": "切换助手显示",
|
||||
"toggle_show_topics": "切换话题显示",
|
||||
"copy_last_message": "复制上一条消息",
|
||||
"search_message": "搜索消息"
|
||||
"search_message": "搜索消息",
|
||||
"mini_window": "快捷助手"
|
||||
},
|
||||
"theme.auto": "跟随系统",
|
||||
"theme.dark": "深色主题",
|
||||
@ -540,7 +545,8 @@
|
||||
},
|
||||
"tray": {
|
||||
"quit": "退出",
|
||||
"show_window": "显示窗口"
|
||||
"show_window": "显示窗口",
|
||||
"show_mini_window": "快捷助手"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "知识图谱",
|
||||
@ -622,7 +628,31 @@
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"summarize": "你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号"
|
||||
"title": "你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号",
|
||||
"explanation": "帮我解释一下这个概念",
|
||||
"summarize": "帮我总结一下这段话"
|
||||
},
|
||||
"miniwindow": {
|
||||
"feature": {
|
||||
"chat": "回答此问题",
|
||||
"translate": "文本翻译",
|
||||
"summary": "内容总结",
|
||||
"explanation": "解释说明"
|
||||
},
|
||||
"clipboard": {
|
||||
"empty": "剪贴板为空"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": {
|
||||
"title": "你想对下方文字做什么",
|
||||
"empty": "询问 {{model}} 获取帮助..."
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"esc": "按 ESC {{action}}",
|
||||
"esc_close": "关闭窗口",
|
||||
"esc_back": "返回"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -385,6 +385,10 @@
|
||||
"webdav.syncError": "備份錯誤",
|
||||
"webdav.lastSync": "上次同步時間"
|
||||
},
|
||||
"quickAssistant": {
|
||||
"title": "快捷助手",
|
||||
"click_tray_to_show": "點擊系統托盤圖標打開"
|
||||
},
|
||||
"display.title": "顯示設定",
|
||||
"font_size.title": "訊息字體大小",
|
||||
"general": "一般設定",
|
||||
@ -506,7 +510,8 @@
|
||||
"toggle_show_assistants": "切換助手顯示",
|
||||
"toggle_show_topics": "切換話題顯示",
|
||||
"copy_last_message": "複製上一条消息",
|
||||
"search_message": "搜索消息"
|
||||
"search_message": "搜索消息",
|
||||
"mini_window": "快捷助手"
|
||||
},
|
||||
"theme.auto": "自動",
|
||||
"theme.dark": "深色主題",
|
||||
@ -539,7 +544,8 @@
|
||||
},
|
||||
"tray": {
|
||||
"quit": "退出",
|
||||
"show_window": "顯示視窗"
|
||||
"show_window": "顯示視窗",
|
||||
"show_mini_window": "快捷助手"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "知識圖譜",
|
||||
@ -621,7 +627,31 @@
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"summarize": "你是一名擅長會話的助理,你需要將用戶的會話總結為 10 個字以內的標題,標題語言與用戶的首要語言一致,不要使用標點符號和其他特殊符號"
|
||||
"title": "你是一名擅長會話的助理,你需要將用戶的會話總結為 10 個字以內的標題,標題語言與用戶的首要語言一致,不要使用標點符號和其他特殊符號",
|
||||
"explanation": "幫我解釋一下這個概念",
|
||||
"summarize": "幫我總結一下這段話"
|
||||
},
|
||||
"miniwindow": {
|
||||
"feature": {
|
||||
"chat": "回答此問題",
|
||||
"translate": "文本翻譯",
|
||||
"summary": "內容總結",
|
||||
"explanation": "解釋說明"
|
||||
},
|
||||
"clipboard": {
|
||||
"empty": "剪貼板為空"
|
||||
},
|
||||
"input": {
|
||||
"placeholder": {
|
||||
"title": "你想對下方文字做什麼",
|
||||
"empty": "詢問 {{model}} 獲取幫助..."
|
||||
}
|
||||
},
|
||||
"footer": {
|
||||
"esc": "按 ESC {{action}}",
|
||||
"esc_close": "關閉窗口",
|
||||
"esc_back": "返回"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,6 +3,13 @@ import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
import { startAutoSync } from './services/BackupService'
|
||||
import store from './store'
|
||||
|
||||
function initSpinner() {
|
||||
const spinner = document.getElementById('spinner')
|
||||
if (spinner && window.location.hash !== '#/mini') {
|
||||
spinner.style.display = 'flex'
|
||||
}
|
||||
}
|
||||
|
||||
function initKeyv() {
|
||||
window.keyv = new KeyvStorage()
|
||||
window.keyv.init()
|
||||
@ -17,5 +24,6 @@ function initAutoSync() {
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
initSpinner()
|
||||
initKeyv()
|
||||
initAutoSync()
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import './assets/styles/index.scss'
|
||||
import './init'
|
||||
|
||||
import ReactDOM from 'react-dom/client'
|
||||
|
||||
import App from './App'
|
||||
import MiniApp from './windows/mini/App'
|
||||
|
||||
if (location.hash === '#/mini') {
|
||||
document.getElementById('spinner')?.remove()
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<MiniApp />)
|
||||
} else {
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)
|
||||
}
|
||||
|
||||
@ -120,7 +120,6 @@ const MessageItem: FC<Props> = ({
|
||||
messages.findIndex((m) => m.id === message.id)
|
||||
),
|
||||
assistant: assistantWithModel,
|
||||
topic,
|
||||
onResponse: (msg) => {
|
||||
setMessage(msg)
|
||||
if (msg.status !== 'pending') {
|
||||
|
||||
@ -52,7 +52,7 @@ const WebDavSettings: FC = () => {
|
||||
return
|
||||
}
|
||||
setBackuping(true)
|
||||
await backupToWebdav()
|
||||
await backupToWebdav({ showMessage: true })
|
||||
setBackuping(false)
|
||||
}
|
||||
|
||||
|
||||
@ -56,9 +56,9 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
<div style={{ marginBottom: 10 }}>{t('settings.models.topic_naming_prompt')}</div>
|
||||
<Input.TextArea
|
||||
rows={4}
|
||||
value={topicNamingPrompt || t('prompts.summarize')}
|
||||
value={topicNamingPrompt || t('prompts.title')}
|
||||
onChange={(e) => dispatch(setTopicNamingPrompt(e.target.value.trim()))}
|
||||
placeholder={t('prompts.summarize')}
|
||||
placeholder={t('prompts.title')}
|
||||
/>
|
||||
{topicNamingPrompt && (
|
||||
<Button style={{ marginTop: 10 }} onClick={handleReset}>
|
||||
|
||||
54
src/renderer/src/pages/settings/QuickAssistantSettings.tsx
Normal file
54
src/renderer/src/pages/settings/QuickAssistantSettings.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setClickTrayToShowQuickAssistant } from '@renderer/store/settings'
|
||||
import HomeWindow from '@renderer/windows/mini/home/HomeWindow'
|
||||
import { Switch } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '.'
|
||||
|
||||
const QuickAssistantSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const { clickTrayToShowQuickAssistant, setTray } = useSettings()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const handleClickTrayToShowQuickAssistant = async (checked: boolean) => {
|
||||
dispatch(setClickTrayToShowQuickAssistant(checked))
|
||||
await window.api.config.set('clickTrayToShowQuickAssistant', checked)
|
||||
if (checked) {
|
||||
setTray(true)
|
||||
window.api.setTray(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingContainer theme={theme}>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.quickAssistant.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.quickAssistant.click_tray_to_show')}</SettingRowTitle>
|
||||
<Switch checked={clickTrayToShowQuickAssistant} onChange={handleClickTrayToShowQuickAssistant} />
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
<AssistantContainer onClick={() => {}}>
|
||||
<HomeWindow />
|
||||
</AssistantContainer>
|
||||
</SettingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const AssistantContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 460px;
|
||||
background-color: var(--color-background);
|
||||
border-radius: 10px;
|
||||
border: 0.5px solid var(--color-border);
|
||||
margin: 0 auto;
|
||||
`
|
||||
|
||||
export default QuickAssistantSettings
|
||||
@ -3,6 +3,7 @@ import {
|
||||
InfoCircleOutlined,
|
||||
LayoutOutlined,
|
||||
MacCommandOutlined,
|
||||
RocketOutlined,
|
||||
SaveOutlined,
|
||||
SettingOutlined
|
||||
} from '@ant-design/icons'
|
||||
@ -19,6 +20,7 @@ import DisplaySettings from './DisplaySettings/DisplaySettings'
|
||||
import GeneralSettings from './GeneralSettings'
|
||||
import ModelSettings from './ModalSettings/ModelSettings'
|
||||
import ProvidersList from './ProviderSettings'
|
||||
import QuickAssistantSettings from './QuickAssistantSettings'
|
||||
import ShortcutSettings from './ShortcutSettings'
|
||||
|
||||
const SettingsPage: FC = () => {
|
||||
@ -68,6 +70,12 @@ const SettingsPage: FC = () => {
|
||||
{t('settings.shortcuts.title')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/quickAssistant">
|
||||
<MenuItem className={isRoute('/settings/quickAssistant')}>
|
||||
<RocketOutlined />
|
||||
{t('settings.quickAssistant.title')}
|
||||
</MenuItem>
|
||||
</MenuItemLink>
|
||||
<MenuItemLink to="/settings/data">
|
||||
<MenuItem className={isRoute('/settings/data')}>
|
||||
<SaveOutlined />
|
||||
@ -88,6 +96,7 @@ const SettingsPage: FC = () => {
|
||||
<Route path="general/*" element={<GeneralSettings />} />
|
||||
<Route path="display" element={<DisplaySettings />} />
|
||||
<Route path="data/*" element={<DataSettings />} />
|
||||
<Route path="quickAssistant" element={<QuickAssistantSettings />} />
|
||||
<Route path="shortcut" element={<ShortcutSettings />} />
|
||||
<Route path="about" element={<AboutSettings />} />
|
||||
</Routes>
|
||||
|
||||
@ -190,7 +190,7 @@ export default class AnthropicProvider extends BaseProvider {
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.summarize')
|
||||
content: (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title')
|
||||
}
|
||||
|
||||
const userMessage = {
|
||||
|
||||
@ -269,7 +269,7 @@ export default class GeminiProvider extends BaseProvider {
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.summarize')
|
||||
content: (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title')
|
||||
}
|
||||
|
||||
const userMessage = {
|
||||
|
||||
@ -322,7 +322,7 @@ export default class OpenAIProvider extends BaseProvider {
|
||||
|
||||
const systemMessage = {
|
||||
role: 'system',
|
||||
content: getStoreSetting('topicNamingPrompt') || i18n.t('prompts.summarize')
|
||||
content: getStoreSetting('topicNamingPrompt') || i18n.t('prompts.title')
|
||||
}
|
||||
|
||||
const userMessage = {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { setGenerating } from '@renderer/store/runtime'
|
||||
import { Assistant, Message, Model, Provider, Suggestion, Topic } from '@renderer/types'
|
||||
import { Assistant, Message, Model, Provider, Suggestion } from '@renderer/types'
|
||||
import { isEmpty } from 'lodash'
|
||||
|
||||
import AiProvider from '../providers/AiProvider'
|
||||
@ -24,7 +24,6 @@ export async function fetchChatCompletion({
|
||||
}: {
|
||||
message: Message
|
||||
messages: Message[]
|
||||
topic: Topic
|
||||
assistant: Assistant
|
||||
onResponse: (message: Message) => void
|
||||
}) {
|
||||
|
||||
@ -181,10 +181,8 @@ export function startAutoSync() {
|
||||
try {
|
||||
console.log('[AutoSync] Performing auto backup...')
|
||||
await backupToWebdav({ showMessage: false })
|
||||
window.message.success({ content: i18n.t('message.backup.success'), key: 'webdav-auto-sync' })
|
||||
} catch (error) {
|
||||
console.error('[AutoSync] Auto backup failed:', error)
|
||||
window.message.error({ content: i18n.t('message.backup.failed'), key: 'webdav-auto-sync' })
|
||||
} finally {
|
||||
isAutoBackupRunning = false
|
||||
scheduleNextBackup()
|
||||
|
||||
@ -30,7 +30,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 56,
|
||||
version: 57,
|
||||
blacklist: ['runtime'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -407,6 +407,7 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setDefaultModel: (state, action: PayloadAction<{ model: Model }>) => {
|
||||
state.defaultModel = action.payload.model
|
||||
window.electron.ipcRenderer.send('miniwindow-reload')
|
||||
},
|
||||
setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => {
|
||||
state.topicNamingModel = action.payload.model
|
||||
|
||||
@ -812,6 +812,16 @@ const migrateConfig = {
|
||||
enabled: false
|
||||
})
|
||||
return state
|
||||
},
|
||||
'57': (state: RootState) => {
|
||||
state.shortcuts.shortcuts.push({
|
||||
key: 'mini_window',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', 'E'],
|
||||
editable: true,
|
||||
enabled: false,
|
||||
system: true
|
||||
})
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -61,6 +61,7 @@ export interface SettingsState {
|
||||
disabled: SidebarIcon[]
|
||||
}
|
||||
narrowMode: boolean
|
||||
clickTrayToShowQuickAssistant: boolean
|
||||
}
|
||||
|
||||
const initialState: SettingsState = {
|
||||
@ -105,7 +106,8 @@ const initialState: SettingsState = {
|
||||
visible: DEFAULT_SIDEBAR_ICONS,
|
||||
disabled: []
|
||||
},
|
||||
narrowMode: false
|
||||
narrowMode: false,
|
||||
clickTrayToShowQuickAssistant: false
|
||||
}
|
||||
|
||||
const settingsSlice = createSlice({
|
||||
@ -240,6 +242,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setNarrowMode: (state, action: PayloadAction<boolean>) => {
|
||||
state.narrowMode = action.payload
|
||||
},
|
||||
setClickTrayToShowQuickAssistant: (state, action: PayloadAction<boolean>) => {
|
||||
state.clickTrayToShowQuickAssistant = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -285,7 +290,8 @@ export const {
|
||||
setCustomCss,
|
||||
setTopicNamingPrompt,
|
||||
setSidebarIcons,
|
||||
setNarrowMode
|
||||
setNarrowMode,
|
||||
setClickTrayToShowQuickAssistant
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@ -51,6 +51,13 @@ const initialState: ShortcutsState = {
|
||||
editable: true,
|
||||
enabled: true,
|
||||
system: false
|
||||
},
|
||||
{
|
||||
key: 'mini_window',
|
||||
shortcut: [isMac ? 'Command' : 'Ctrl', 'E'],
|
||||
editable: true,
|
||||
enabled: false,
|
||||
system: true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
146
src/renderer/src/windows/menu/menu.html
Normal file
146
src/renderer/src/windows/menu/menu.html
Normal file
@ -0,0 +1,146 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Selection Menu</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: rgba(255, 255, 255, 0.95);
|
||||
--button-bg: #f5f5f5;
|
||||
--button-hover: #e8e8e8;
|
||||
--text-color: #333;
|
||||
--border-color: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg-color: rgba(80, 80, 80, 0.95);
|
||||
--button-bg: #2c2c2c;
|
||||
--button-hover: #383838;
|
||||
--text-color: #e0e0e0;
|
||||
--border-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 280px;
|
||||
height: 40px;
|
||||
background: var(--bg-color);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
width: 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.drag-handle::before,
|
||||
.drag-handle::after {
|
||||
content: '';
|
||||
width: 2px;
|
||||
height: 16px;
|
||||
background-color: var(--border-color);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
menu {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 40px;
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 32px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--text-color);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: var(--button-hover);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="drag-handle"></div>
|
||||
<menu>
|
||||
<button data-action="chat">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M20,2H4C2.9,2,2,2.9,2,4v18l4-4h14c1.1,0,2-0.9,2-2V4C22,2.9,21.1,2,20,2z M20,16H6l-2,2V4h16V16z" />
|
||||
</svg>
|
||||
提问
|
||||
</button>
|
||||
<button data-action="explanation">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path
|
||||
d="M12,2C6.48,2,2,6.48,2,12s4.48,10,10,10s10-4.48,10-10S17.52,2,12,2z M13,17h-2v-6h2V17z M13,9h-2V7h2V9z" />
|
||||
</svg>
|
||||
释义
|
||||
</button>
|
||||
<button data-action="translate">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M6 4h12v2H6zM6 10h12v2H6zM6 16h8v2H6z" />
|
||||
</svg>
|
||||
翻译
|
||||
</button>
|
||||
<button data-action="summary">
|
||||
<svg viewBox="0 0 24 24">
|
||||
<path d="M14,17H4v2h10V17z M20,9H4v2h16V9z M4,15h16v-2H4V15z M4,5v2h16V5H4z" />
|
||||
</svg>
|
||||
总结
|
||||
</button>
|
||||
</menu>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('button').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const action = button.getAttribute('data-action')
|
||||
window.api.selectionMenu.action(action)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
29
src/renderer/src/windows/mini/App.tsx
Normal file
29
src/renderer/src/windows/mini/App.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import '@renderer/databases'
|
||||
|
||||
import store, { persistor } from '@renderer/store'
|
||||
import { Provider } from 'react-redux'
|
||||
import { PersistGate } from 'redux-persist/integration/react'
|
||||
|
||||
import AntdProvider from '../../context/AntdProvider'
|
||||
import { SyntaxHighlighterProvider } from '../../context/SyntaxHighlighterProvider'
|
||||
import { ThemeProvider } from '../../context/ThemeProvider'
|
||||
import { ThemeMode } from '../../types'
|
||||
import HomeWindow from './home/HomeWindow'
|
||||
|
||||
function MiniWindow(): JSX.Element {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ThemeProvider defaultTheme={ThemeMode.auto}>
|
||||
<AntdProvider>
|
||||
<SyntaxHighlighterProvider>
|
||||
<PersistGate loading={null} persistor={persistor}>
|
||||
<HomeWindow />
|
||||
</PersistGate>
|
||||
</SyntaxHighlighterProvider>
|
||||
</AntdProvider>
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export default MiniWindow
|
||||
34
src/renderer/src/windows/mini/chat/ChatWindow.tsx
Normal file
34
src/renderer/src/windows/mini/chat/ChatWindow.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import Messages from './Messages'
|
||||
|
||||
interface Props {
|
||||
route: string
|
||||
}
|
||||
|
||||
const ChatWindow: FC<Props> = ({ route }) => {
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
|
||||
return (
|
||||
<Main className="bubble">
|
||||
<Messages assistant={{ ...defaultAssistant, model: getDefaultModel() }} route={route} />
|
||||
</Main>
|
||||
)
|
||||
}
|
||||
|
||||
const Main = styled(Scrollbar)`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: auto;
|
||||
-webkit-app-region: none;
|
||||
background-color: transparent !important;
|
||||
max-height: 100%;
|
||||
`
|
||||
|
||||
export default ChatWindow
|
||||
443
src/renderer/src/windows/mini/chat/Inputbar.tsx
Normal file
443
src/renderer/src/windows/mini/chat/Inputbar.tsx
Normal file
@ -0,0 +1,443 @@
|
||||
import { ClearOutlined, PauseCircleOutlined, QuestionCircleOutlined } from '@ant-design/icons'
|
||||
import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isVisionModel } from '@renderer/config/models'
|
||||
import { useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import AttachmentButton from '@renderer/pages/home/Inputbar/AttachmentButton'
|
||||
import AttachmentPreview from '@renderer/pages/home/Inputbar/AttachmentPreview'
|
||||
import KnowledgeBaseButton from '@renderer/pages/home/Inputbar/KnowledgeBaseButton'
|
||||
import SendMessageButton from '@renderer/pages/home/Inputbar/SendMessageButton'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
||||
import { FileType, KnowledgeBase, Message } from '@renderer/types'
|
||||
import { delay, getFileExtension, uuid } from '@renderer/utils'
|
||||
import { documentExts, imageExts, textExts } from '@shared/config/constant'
|
||||
import { Button, Popconfirm, Tooltip } from 'antd'
|
||||
import TextArea, { TextAreaRef } from 'antd/es/input/TextArea'
|
||||
import dayjs from 'dayjs'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const Inputbar: FC = () => {
|
||||
const [text, setText] = useState('')
|
||||
const [inputFocus, setInputFocus] = useState(false)
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { defaultModel } = useDefaultModel()
|
||||
const assistant = defaultAssistant
|
||||
const model = defaultModel
|
||||
const {
|
||||
sendMessageShortcut,
|
||||
fontSize,
|
||||
pasteLongTextAsFile,
|
||||
pasteLongTextThreshold,
|
||||
language,
|
||||
autoTranslateWithSpace
|
||||
} = useSettings()
|
||||
const [expended, setExpend] = useState(false)
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
const textareaRef = useRef<TextAreaRef>(null)
|
||||
const [files, setFiles] = useState<FileType[]>([])
|
||||
const { t } = useTranslation()
|
||||
const containerRef = useRef(null)
|
||||
const { searching } = useRuntime()
|
||||
const dispatch = useAppDispatch()
|
||||
const [spaceClickCount, setSpaceClickCount] = useState(0)
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase>()
|
||||
|
||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||
|
||||
const inputEmpty = isEmpty(text.trim()) && files.length === 0
|
||||
|
||||
const sendMessage = useCallback(async () => {
|
||||
if (generating) {
|
||||
return
|
||||
}
|
||||
|
||||
if (inputEmpty) {
|
||||
return
|
||||
}
|
||||
|
||||
const message: Message = {
|
||||
id: uuid(),
|
||||
role: 'user',
|
||||
content: text,
|
||||
assistantId: assistant.id,
|
||||
topicId: assistant.topics[0].id || uuid(),
|
||||
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
type: 'text',
|
||||
status: 'success'
|
||||
}
|
||||
|
||||
if (selectedKnowledgeBase) {
|
||||
message.knowledgeBaseIds = [selectedKnowledgeBase.id]
|
||||
}
|
||||
|
||||
if (files.length > 0) {
|
||||
message.files = await FileManager.uploadFiles(files)
|
||||
}
|
||||
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||
|
||||
setText('')
|
||||
setFiles([])
|
||||
setTimeout(() => setText(''), 500)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
|
||||
setExpend(false)
|
||||
}, [generating, inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files])
|
||||
|
||||
const translate = async () => {
|
||||
if (isTranslating) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setIsTranslating(true)
|
||||
const translatedText = await translateText(text, 'english')
|
||||
translatedText && setText(translatedText)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
} catch (error) {
|
||||
console.error('Translation failed:', error)
|
||||
} finally {
|
||||
setIsTranslating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
const isEnterPressed = event.keyCode == 13
|
||||
|
||||
if (autoTranslateWithSpace) {
|
||||
if (event.key === ' ') {
|
||||
setSpaceClickCount((prev) => prev + 1)
|
||||
|
||||
if (spaceClickTimer.current) {
|
||||
clearTimeout(spaceClickTimer.current)
|
||||
}
|
||||
|
||||
spaceClickTimer.current = setTimeout(() => {
|
||||
setSpaceClickCount(0)
|
||||
}, 200)
|
||||
|
||||
if (spaceClickCount === 2) {
|
||||
console.log('Triple space detected - trigger translation')
|
||||
setSpaceClickCount(0)
|
||||
setIsTranslating(true)
|
||||
translate()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expended) {
|
||||
if (event.key === 'Escape') {
|
||||
return setExpend(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (sendMessageShortcut === 'Enter' && isEnterPressed) {
|
||||
if (event.shiftKey) {
|
||||
return
|
||||
}
|
||||
sendMessage()
|
||||
return event.preventDefault()
|
||||
}
|
||||
|
||||
if (sendMessageShortcut === 'Shift+Enter' && isEnterPressed && event.shiftKey) {
|
||||
sendMessage()
|
||||
return event.preventDefault()
|
||||
}
|
||||
|
||||
if (sendMessageShortcut === 'Ctrl+Enter' && isEnterPressed && event.ctrlKey) {
|
||||
sendMessage()
|
||||
return event.preventDefault()
|
||||
}
|
||||
|
||||
if (sendMessageShortcut === 'Command+Enter' && isEnterPressed && event.metaKey) {
|
||||
sendMessage()
|
||||
return event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
const clearTopic = async () => {
|
||||
if (generating) {
|
||||
onPause()
|
||||
await delay(1)
|
||||
}
|
||||
EventEmitter.emit(EVENT_NAMES.CLEAR_MESSAGES)
|
||||
}
|
||||
|
||||
const onPause = () => {
|
||||
window.keyv.set(EVENT_NAMES.CHAT_COMPLETION_PAUSED, true)
|
||||
store.dispatch(setGenerating(false))
|
||||
}
|
||||
|
||||
const resizeTextArea = () => {
|
||||
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||
if (textArea) {
|
||||
textArea.style.height = 'auto'
|
||||
textArea.style.height = textArea?.scrollHeight > 400 ? '400px' : `${textArea?.scrollHeight}px`
|
||||
}
|
||||
}
|
||||
|
||||
const onInput = () => !expended && resizeTextArea()
|
||||
|
||||
const onPaste = useCallback(
|
||||
async (event: ClipboardEvent) => {
|
||||
for (const file of event.clipboardData?.files || []) {
|
||||
event.preventDefault()
|
||||
|
||||
if (file.path === '') {
|
||||
if (file.type.startsWith('image/')) {
|
||||
const tempFilePath = await window.api.file.create(file.name)
|
||||
const arrayBuffer = await file.arrayBuffer()
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
await window.api.file.write(tempFilePath, uint8Array)
|
||||
const selectedFile = await window.api.file.get(tempFilePath)
|
||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (file.path) {
|
||||
if (supportExts.includes(getFileExtension(file.path))) {
|
||||
const selectedFile = await window.api.file.get(file.path)
|
||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pasteLongTextAsFile) {
|
||||
const item = event.clipboardData?.items[0]
|
||||
if (item && item.kind === 'string' && item.type === 'text/plain') {
|
||||
item.getAsString(async (pasteText) => {
|
||||
if (pasteText.length > pasteLongTextThreshold) {
|
||||
const tempFilePath = await window.api.file.create('pasted_text.txt')
|
||||
await window.api.file.write(tempFilePath, pasteText)
|
||||
const selectedFile = await window.api.file.get(tempFilePath)
|
||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
setText(text)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
[pasteLongTextAsFile, pasteLongTextThreshold, supportExts, text]
|
||||
)
|
||||
|
||||
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const files = Array.from(e.dataTransfer.files)
|
||||
|
||||
files.forEach(async (file) => {
|
||||
if (supportExts.includes(getFileExtension(file.path))) {
|
||||
const selectedFile = await window.api.file.get(file.path)
|
||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const onTranslated = (translatedText: string) => {
|
||||
setText(translatedText)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
textareaRef.current?.focus()
|
||||
}, [assistant])
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (spaceClickTimer.current) {
|
||||
clearTimeout(spaceClickTimer.current)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => {
|
||||
setSelectedKnowledgeBase(base)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container onDragOver={handleDragOver} onDrop={handleDrop}>
|
||||
<AttachmentPreview files={files} setFiles={setFiles} />
|
||||
<InputBarContainer id="inputbar" className={inputFocus ? 'focus' : ''} ref={containerRef}>
|
||||
<Textarea
|
||||
value={text}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={isTranslating ? t('chat.input.translating') : t('chat.input.placeholder')}
|
||||
autoFocus
|
||||
contextMenu="true"
|
||||
variant="borderless"
|
||||
rows={1}
|
||||
ref={textareaRef}
|
||||
style={{ fontSize }}
|
||||
styles={{ textarea: TextareaStyle }}
|
||||
onFocus={() => setInputFocus(true)}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
onInput={onInput}
|
||||
disabled={searching}
|
||||
onPaste={(e) => onPaste(e.nativeEvent)}
|
||||
onClick={() => searching && dispatch(setSearching(false))}
|
||||
/>
|
||||
<Toolbar>
|
||||
<ToolbarMenu>
|
||||
<Tooltip placement="top" title={t('chat.input.clear')} arrow>
|
||||
<Popconfirm
|
||||
title={t('chat.input.clear.content')}
|
||||
placement="top"
|
||||
onConfirm={clearTopic}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
|
||||
okText={t('chat.input.clear')}>
|
||||
<ToolbarButton type="text">
|
||||
<ClearOutlined />
|
||||
</ToolbarButton>
|
||||
</Popconfirm>
|
||||
</Tooltip>
|
||||
<KnowledgeBaseButton
|
||||
selectedBase={selectedKnowledgeBase}
|
||||
onSelect={handleKnowledgeBaseSelect}
|
||||
ToolbarButton={ToolbarButton}
|
||||
disabled={files.length > 0}
|
||||
/>
|
||||
<AttachmentButton
|
||||
model={model}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
ToolbarButton={ToolbarButton}
|
||||
disabled={!!selectedKnowledgeBase}
|
||||
/>
|
||||
</ToolbarMenu>
|
||||
<ToolbarMenu>
|
||||
{!language.startsWith('en') && (
|
||||
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
||||
)}
|
||||
{generating && (
|
||||
<Tooltip placement="top" title={t('chat.input.pause')} arrow>
|
||||
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
|
||||
<PauseCircleOutlined style={{ color: 'var(--color-error)', fontSize: 20 }} />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
</InputBarContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
-webkit-app-region: none;
|
||||
`
|
||||
|
||||
const InputBarContainer = styled.div`
|
||||
border: 1px solid var(--color-border);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
margin: 10px;
|
||||
border-radius: 10px;
|
||||
`
|
||||
|
||||
const TextareaStyle: CSSProperties = {
|
||||
paddingLeft: 0,
|
||||
padding: '10px 15px 8px'
|
||||
}
|
||||
|
||||
const Textarea = styled(TextArea)`
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-family: Ubuntu;
|
||||
resize: none !important;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
&.ant-input {
|
||||
line-height: 1.4;
|
||||
}
|
||||
`
|
||||
|
||||
const Toolbar = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 0 8px;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 4px;
|
||||
height: 36px;
|
||||
`
|
||||
|
||||
const ToolbarMenu = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
`
|
||||
|
||||
const ToolbarButton = styled(Button)`
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 17px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
color: var(--color-icon);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
&.anticon,
|
||||
&.iconfont {
|
||||
transition: all 0.3s ease;
|
||||
color: var(--color-icon);
|
||||
}
|
||||
.icon-a-addchat {
|
||||
font-size: 19px;
|
||||
margin-bottom: -2px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-background-soft);
|
||||
.anticon,
|
||||
.iconfont {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
background-color: var(--color-primary) !important;
|
||||
.anticon,
|
||||
.iconfont {
|
||||
color: var(--color-white-soft);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default Inputbar
|
||||
122
src/renderer/src/windows/mini/chat/Message.tsx
Normal file
122
src/renderer/src/windows/mini/chat/Message.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
import { FONT_FAMILY } from '@renderer/config/constant'
|
||||
import { useModel } from '@renderer/hooks/useModel'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import MessageContent from '@renderer/pages/home/Messages/MessageContent'
|
||||
import MessageErrorBoundary from '@renderer/pages/home/Messages/MessageErrorBoundary'
|
||||
import { fetchChatCompletion } from '@renderer/services/ApiService'
|
||||
import { getDefaultAssistant, getDefaultModel } from '@renderer/services/AssistantService'
|
||||
import { Message } from '@renderer/types'
|
||||
import { Dispatch, FC, memo, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
index?: number
|
||||
total?: number
|
||||
route: string
|
||||
onGetMessages?: () => Message[]
|
||||
onSetMessages?: Dispatch<SetStateAction<Message[]>>
|
||||
}
|
||||
|
||||
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) =>
|
||||
isBubbleStyle ? (isAssistantMessage ? 'transparent' : 'var(--chat-background-user)') : undefined
|
||||
|
||||
const MessageItem: FC<Props> = ({ message: _message, index, total, route, onSetMessages, onGetMessages }) => {
|
||||
const [message, setMessage] = useState(_message)
|
||||
const model = useModel(message.modelId)
|
||||
const isBubbleStyle = true
|
||||
const { messageFont, fontSize } = useSettings()
|
||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
|
||||
const fontFamily = useMemo(() => {
|
||||
return messageFont === 'serif' ? FONT_FAMILY.replace('sans-serif', 'serif').replace('Ubuntu, ', '') : FONT_FAMILY
|
||||
}, [messageFont])
|
||||
|
||||
const messageBackground = getMessageBackground(true, isAssistantMessage)
|
||||
|
||||
useEffect(() => {
|
||||
if (onGetMessages && onSetMessages) {
|
||||
if (message.status === 'sending') {
|
||||
const messages = onGetMessages()
|
||||
fetchChatCompletion({
|
||||
message,
|
||||
messages: messages
|
||||
.filter((m) => !m.status.includes('ing'))
|
||||
.slice(
|
||||
0,
|
||||
messages.findIndex((m) => m.id === message.id)
|
||||
),
|
||||
assistant: { ...getDefaultAssistant(), model: getDefaultModel() },
|
||||
onResponse: (msg) => {
|
||||
setMessage(msg)
|
||||
if (msg.status !== 'pending') {
|
||||
const _messages = messages.map((m) => (m.id === msg.id ? msg : m))
|
||||
onSetMessages(_messages)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [message.status])
|
||||
|
||||
if (['summary', 'explanation'].includes(route) && index === total - 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageContainer
|
||||
key={message.id}
|
||||
ref={messageContainerRef}
|
||||
style={{ ...(isBubbleStyle ? { alignItems: isAssistantMessage ? 'start' : 'end' } : {}) }}>
|
||||
<MessageContentContainer
|
||||
className="message-content-container"
|
||||
style={{
|
||||
fontFamily,
|
||||
fontSize,
|
||||
background: messageBackground,
|
||||
...(isAssistantMessage ? { paddingLeft: 5, paddingRight: 5 } : {})
|
||||
}}>
|
||||
<MessageErrorBoundary>
|
||||
<MessageContent message={message} model={model} />
|
||||
</MessageErrorBoundary>
|
||||
</MessageContentContainer>
|
||||
</MessageContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const MessageContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease;
|
||||
&.message-highlight {
|
||||
background-color: var(--color-primary-mute);
|
||||
}
|
||||
.menubar {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
&.show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.menubar {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MessageContentContainer = styled.div`
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-left: 46px;
|
||||
margin-top: 5px;
|
||||
`
|
||||
|
||||
export default memo(MessageItem)
|
||||
73
src/renderer/src/windows/mini/chat/Messages.tsx
Normal file
73
src/renderer/src/windows/mini/chat/Messages.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { getAssistantMessage } from '@renderer/services/MessagesService'
|
||||
import { Assistant, Message } from '@renderer/types'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import MessageItem from './Message'
|
||||
|
||||
interface Props {
|
||||
assistant: Assistant
|
||||
route: string
|
||||
}
|
||||
|
||||
interface ContainerProps {
|
||||
right?: boolean
|
||||
}
|
||||
|
||||
const Messages: FC<Props> = ({ assistant, route }) => {
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const messagesRef = useRef(messages)
|
||||
|
||||
messagesRef.current = messages
|
||||
|
||||
const onSendMessage = useCallback(
|
||||
async (message: Message) => {
|
||||
setMessages((prev) => {
|
||||
const assistantMessage = getAssistantMessage({ assistant, topic: assistant.topics[0] })
|
||||
const messages = prev.concat([message, assistantMessage])
|
||||
return messages
|
||||
})
|
||||
},
|
||||
[assistant]
|
||||
)
|
||||
|
||||
const onGetMessages = useCallback(() => {
|
||||
return messagesRef.current
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribes = [EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage)]
|
||||
return () => unsubscribes.forEach((unsub) => unsub())
|
||||
}, [assistant.id, onSendMessage])
|
||||
|
||||
return (
|
||||
<Container id="messages" key={assistant.id} ref={containerRef}>
|
||||
{[...messages].reverse().map((message, index) => (
|
||||
<MessageItem
|
||||
key={message.id}
|
||||
message={message}
|
||||
index={index}
|
||||
total={messages.length}
|
||||
onSetMessages={setMessages}
|
||||
onGetMessages={onGetMessages}
|
||||
route={route}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled(Scrollbar)<ContainerProps>`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding-bottom: 20px;
|
||||
overflow-x: hidden;
|
||||
min-width: 100%;
|
||||
background-color: transparent !important;
|
||||
`
|
||||
|
||||
export default Messages
|
||||
203
src/renderer/src/windows/mini/home/HomeWindow.tsx
Normal file
203
src/renderer/src/windows/mini/home/HomeWindow.tsx
Normal file
@ -0,0 +1,203 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { EVENT_NAMES } from '@renderer/services/EventService'
|
||||
import { EventEmitter } from '@renderer/services/EventService'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Divider } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ChatWindow from '../chat/ChatWindow'
|
||||
import ClipboardPreview from './components/ClipboardPreview'
|
||||
import FeatureMenus from './components/FeatureMenus'
|
||||
import Footer from './components/Footer'
|
||||
import InputBar from './components/InputBar'
|
||||
import Translate from './Translate'
|
||||
|
||||
const HomeWindow: FC = () => {
|
||||
const [route, setRoute] = useState<'home' | 'chat' | 'translate' | 'summary' | 'explanation'>('home')
|
||||
const [clipboardText, setClipboardText] = useState('')
|
||||
const [selectedText, setSelectedText] = useState('')
|
||||
const [text, setText] = useState('')
|
||||
const { defaultAssistant } = useDefaultAssistant()
|
||||
const { defaultModel: model } = useDefaultModel()
|
||||
const { t } = useTranslation()
|
||||
const textRef = useRef(text)
|
||||
|
||||
const referenceText = selectedText || clipboardText
|
||||
|
||||
textRef.current = `${referenceText}\n\n${text}`
|
||||
|
||||
const isMiniWindow = window.location.hash === '#/mini'
|
||||
|
||||
const onReadClipboard = useCallback(async () => {
|
||||
const text = await navigator.clipboard.readText()
|
||||
setClipboardText(text.trim())
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
onReadClipboard()
|
||||
}, [onReadClipboard])
|
||||
|
||||
const onCloseWindow = () => isMiniWindow && window.close()
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Escape') {
|
||||
setText('')
|
||||
setRoute('home')
|
||||
route === 'home' && onCloseWindow()
|
||||
return
|
||||
}
|
||||
|
||||
if (e.key === 'Enter') {
|
||||
if (text.trim() === '') {
|
||||
return
|
||||
}
|
||||
setRoute('chat')
|
||||
onSendMessage()
|
||||
setTimeout(() => setText(''), 100)
|
||||
}
|
||||
}
|
||||
|
||||
const onSendMessage = useCallback(
|
||||
async (prompt?: string) => {
|
||||
setTimeout(() => {
|
||||
const message = {
|
||||
id: uuid(),
|
||||
role: 'user',
|
||||
content: prompt ? `${prompt}\n\n${textRef.current}` : textRef.current,
|
||||
assistantId: defaultAssistant.id,
|
||||
topicId: defaultAssistant.topics[0].id || uuid(),
|
||||
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
|
||||
type: 'text',
|
||||
status: 'success'
|
||||
}
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||
}, 0)
|
||||
},
|
||||
[defaultAssistant]
|
||||
)
|
||||
|
||||
const clearClipboard = () => {
|
||||
setClipboardText('')
|
||||
setSelectedText('')
|
||||
navigator.clipboard.writeText('')
|
||||
}
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
if (route === 'home') {
|
||||
onCloseWindow()
|
||||
} else {
|
||||
setRoute('home')
|
||||
setText('')
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer.on('show-mini-window', () => {
|
||||
onReadClipboard()
|
||||
})
|
||||
|
||||
window.electron.ipcRenderer.on('selection-action', (_, { action, selectedText }) => {
|
||||
console.debug('[HomeWindow] selection-action', action, selectedText)
|
||||
selectedText && setSelectedText(selectedText)
|
||||
action && setRoute(action)
|
||||
action === 'chat' && onSendMessage()
|
||||
})
|
||||
|
||||
return () => {
|
||||
window.electron.ipcRenderer.removeAllListeners('show-mini-window')
|
||||
window.electron.ipcRenderer.removeAllListeners('selection-action')
|
||||
}
|
||||
}, [onReadClipboard, onSendMessage, setRoute])
|
||||
|
||||
if (['chat', 'summary', 'explanation'].includes(route)) {
|
||||
return (
|
||||
<Container>
|
||||
{route === 'chat' && (
|
||||
<>
|
||||
<InputBar
|
||||
text={text}
|
||||
model={model}
|
||||
referenceText={referenceText}
|
||||
placeholder={t('miniwindow.input.placeholder.empty', { model: model.name })}
|
||||
handleKeyDown={handleKeyDown}
|
||||
setText={setText}
|
||||
/>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
</>
|
||||
)}
|
||||
{['summary', 'explanation'].includes(route) && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<ClipboardPreview referenceText={referenceText} clearClipboard={clearClipboard} t={t} />
|
||||
</div>
|
||||
)}
|
||||
<ChatWindow route={route} />
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Footer route={route} onExit={() => setRoute('home')} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
if (route === 'translate') {
|
||||
return (
|
||||
<Container>
|
||||
<Translate text={referenceText} />
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Footer route={route} onExit={() => setRoute('home')} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<InputBar
|
||||
text={text}
|
||||
model={model}
|
||||
referenceText={referenceText}
|
||||
placeholder={
|
||||
referenceText && route === 'home'
|
||||
? t('miniwindow.input.placeholder.title')
|
||||
: t('miniwindow.input.placeholder.empty', { model: model.name })
|
||||
}
|
||||
handleKeyDown={handleKeyDown}
|
||||
setText={setText}
|
||||
/>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<ClipboardPreview referenceText={referenceText} clearClipboard={clearClipboard} t={t} />
|
||||
<Main>
|
||||
<FeatureMenus setRoute={setRoute} onSendMessage={onSendMessage} />
|
||||
</Main>
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
<Footer
|
||||
route={route}
|
||||
onExit={() => {
|
||||
setRoute('home')
|
||||
setText('')
|
||||
onCloseWindow()
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
-webkit-app-region: drag;
|
||||
padding: 8px 10px;
|
||||
background-color: ${isMac ? 'transparent' : 'var(--color-background)'};
|
||||
`
|
||||
|
||||
const Main = styled.main`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
export default HomeWindow
|
||||
176
src/renderer/src/windows/mini/home/Translate.tsx
Normal file
176
src/renderer/src/windows/mini/home/Translate.tsx
Normal file
@ -0,0 +1,176 @@
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import db from '@renderer/databases'
|
||||
import { useDefaultModel } from '@renderer/hooks/useAssistant'
|
||||
import { fetchTranslate } from '@renderer/services/ApiService'
|
||||
import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService'
|
||||
import { Assistant, Message } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Select, Space } from 'antd'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
}
|
||||
|
||||
const Translate: FC<Props> = ({ text }) => {
|
||||
const { t } = useTranslation()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [result, setResult] = useState('')
|
||||
const [targetLanguage, setTargetLanguage] = useState('chinese')
|
||||
const { translateModel } = useDefaultModel()
|
||||
|
||||
const languageOptions = [
|
||||
{
|
||||
value: 'english',
|
||||
label: t('languages.english'),
|
||||
emoji: '🇬🇧'
|
||||
},
|
||||
{
|
||||
value: 'chinese',
|
||||
label: t('languages.chinese'),
|
||||
emoji: '🇨🇳'
|
||||
},
|
||||
{
|
||||
value: 'chinese-traditional',
|
||||
label: t('languages.chinese-traditional'),
|
||||
emoji: '🇭🇰'
|
||||
},
|
||||
{
|
||||
value: 'japanese',
|
||||
label: t('languages.japanese'),
|
||||
emoji: '🇯🇵'
|
||||
},
|
||||
{
|
||||
value: 'korean',
|
||||
label: t('languages.korean'),
|
||||
emoji: '🇰🇷'
|
||||
},
|
||||
{
|
||||
value: 'russian',
|
||||
label: t('languages.russian'),
|
||||
emoji: '🇷🇺'
|
||||
},
|
||||
{
|
||||
value: 'spanish',
|
||||
label: t('languages.spanish'),
|
||||
emoji: '🇪🇸'
|
||||
},
|
||||
{
|
||||
value: 'french',
|
||||
label: t('languages.french'),
|
||||
emoji: '🇫🇷'
|
||||
},
|
||||
{
|
||||
value: 'italian',
|
||||
label: t('languages.italian'),
|
||||
emoji: '🇮🇹'
|
||||
},
|
||||
{
|
||||
value: 'portuguese',
|
||||
label: t('languages.portuguese'),
|
||||
emoji: '🇵🇹'
|
||||
},
|
||||
{
|
||||
value: 'arabic',
|
||||
label: t('languages.arabic'),
|
||||
emoji: '🇸🇦'
|
||||
}
|
||||
]
|
||||
|
||||
const translate = useCallback(async () => {
|
||||
if (!text.trim() || !translateModel) return
|
||||
|
||||
const assistant: Assistant = getDefaultTranslateAssistant(targetLanguage, text)
|
||||
const message: Message = {
|
||||
id: uuid(),
|
||||
role: 'user',
|
||||
content: text,
|
||||
assistantId: assistant.id,
|
||||
topicId: uuid(),
|
||||
modelId: translateModel.id,
|
||||
createdAt: new Date().toISOString(),
|
||||
type: 'text',
|
||||
status: 'sending'
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
const translateText = await fetchTranslate({ message, assistant })
|
||||
setResult(translateText)
|
||||
setLoading(false)
|
||||
}, [text, targetLanguage, translateModel])
|
||||
|
||||
useEffect(() => {
|
||||
// 获取默认目标语言
|
||||
db.settings.get({ id: 'translate:target:language' }).then((targetLang) => {
|
||||
if (targetLang) {
|
||||
setTargetLanguage(targetLang.value)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
translate()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<LanguageSelect>
|
||||
<Select
|
||||
value={targetLanguage}
|
||||
style={{ width: 140 }}
|
||||
optionFilterProp="label"
|
||||
options={languageOptions}
|
||||
onChange={(value) => {
|
||||
setTargetLanguage(value)
|
||||
translate()
|
||||
}}
|
||||
optionRender={(option) => (
|
||||
<Space>
|
||||
<span role="img" aria-label={option.data.label}>
|
||||
{option.data.emoji}
|
||||
</span>
|
||||
{option.label}
|
||||
</Space>
|
||||
)}
|
||||
/>
|
||||
</LanguageSelect>
|
||||
<Main>{loading ? <LoadingText>翻译中...</LoadingText> : <ResultText>{result || text}</ResultText>}</Main>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
padding-right: 0;
|
||||
overflow: hidden;
|
||||
-webkit-app-region: none;
|
||||
`
|
||||
|
||||
const Main = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const ResultText = styled(Scrollbar)`
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
width: 100%;
|
||||
`
|
||||
|
||||
const LoadingText = styled.div`
|
||||
color: var(--color-text-2);
|
||||
font-style: italic;
|
||||
`
|
||||
|
||||
const LanguageSelect = styled.div`
|
||||
margin-bottom: 8px;
|
||||
`
|
||||
|
||||
export default Translate
|
||||
@ -0,0 +1,62 @@
|
||||
import { CloseOutlined } from '@ant-design/icons'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { Typography } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface ClipboardPreviewProps {
|
||||
referenceText: string
|
||||
clearClipboard: () => void
|
||||
t: (key: string) => string
|
||||
}
|
||||
|
||||
const { Paragraph } = Typography
|
||||
|
||||
const ClipboardPreview: FC<ClipboardPreviewProps> = ({ referenceText, clearClipboard, t }) => {
|
||||
if (!referenceText) return null
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<ClipboardContent>
|
||||
<CopyIcon style={{ fontSize: '14px', flexShrink: 0, cursor: 'pointer' }} className="nodrag" />
|
||||
<Paragraph ellipsis={{ rows: 2 }} style={{ margin: '0 12px', fontSize: 12, flex: 1, minWidth: 0 }}>
|
||||
{referenceText || t('miniwindow.clipboard.empty')}
|
||||
</Paragraph>
|
||||
<CloseButton onClick={clearClipboard} className="nodrag">
|
||||
<CloseOutlined style={{ fontSize: '14px' }} />
|
||||
</CloseButton>
|
||||
</ClipboardContent>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 12px;
|
||||
background-color: var(--color-background-opacity);
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
const ClipboardContent = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
color: var(--color-text-secondary);
|
||||
`
|
||||
|
||||
const CloseButton = styled.button`
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-text-secondary);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
`
|
||||
|
||||
export default ClipboardPreview
|
||||
108
src/renderer/src/windows/mini/home/components/FeatureMenus.tsx
Normal file
108
src/renderer/src/windows/mini/home/components/FeatureMenus.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { BulbOutlined, FileTextOutlined, MessageOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { Col } from 'antd'
|
||||
import { Dispatch, FC, SetStateAction } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface FeatureMenusProps {
|
||||
setRoute: Dispatch<SetStateAction<'translate' | 'summary' | 'chat' | 'explanation' | 'home'>>
|
||||
onSendMessage: (prompt?: string) => void
|
||||
}
|
||||
|
||||
const FeatureMenus: FC<FeatureMenusProps> = ({ setRoute, onSendMessage }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <MessageOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.chat'),
|
||||
active: true,
|
||||
onClick: () => {
|
||||
setRoute('chat')
|
||||
onSendMessage()
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <TranslationOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.translate'),
|
||||
onClick: () => setRoute('translate')
|
||||
},
|
||||
{
|
||||
icon: <FileTextOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.summary'),
|
||||
onClick: () => {
|
||||
setRoute('summary')
|
||||
onSendMessage(t('prompts.summarize'))
|
||||
}
|
||||
},
|
||||
{
|
||||
icon: <BulbOutlined style={{ fontSize: '16px', color: 'var(--color-text)' }} />,
|
||||
title: t('miniwindow.feature.explanation'),
|
||||
onClick: () => {
|
||||
setRoute('explanation')
|
||||
onSendMessage(t('prompts.explanation'))
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<FeatureList>
|
||||
<FeatureListWrapper>
|
||||
{features.map((feature, index) => (
|
||||
<Col span={24} key={index}>
|
||||
<FeatureItem onClick={feature.onClick} className={feature.active ? 'active' : ''}>
|
||||
<FeatureIcon>{feature.icon}</FeatureIcon>
|
||||
<FeatureTitle>{feature.title}</FeatureTitle>
|
||||
</FeatureItem>
|
||||
</Col>
|
||||
))}
|
||||
</FeatureListWrapper>
|
||||
</FeatureList>
|
||||
)
|
||||
}
|
||||
|
||||
const FeatureList = styled(Scrollbar)`
|
||||
flex: 1;
|
||||
-webkit-app-region: none;
|
||||
`
|
||||
|
||||
const FeatureListWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const FeatureItem = styled.div`
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
-webkit-app-region: none;
|
||||
border-radius: 8px;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-background-opacity);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--color-background-opacity);
|
||||
}
|
||||
`
|
||||
|
||||
const FeatureIcon = styled.div`
|
||||
color: #fff;
|
||||
`
|
||||
|
||||
const FeatureTitle = styled.h3`
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
export default FeatureMenus
|
||||
37
src/renderer/src/windows/mini/home/components/Footer.tsx
Normal file
37
src/renderer/src/windows/mini/home/components/Footer.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface FooterProps {
|
||||
route: string
|
||||
onExit: () => void
|
||||
}
|
||||
|
||||
const Footer: FC<FooterProps> = ({ route, onExit }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<WindowFooter onClick={() => onExit()}>
|
||||
<FooterText className="nodrag">
|
||||
{t('miniwindow.footer.esc', {
|
||||
action: route === 'home' ? t('miniwindow.footer.esc_close') : t('miniwindow.footer.esc_back')
|
||||
})}
|
||||
</FooterText>
|
||||
</WindowFooter>
|
||||
)
|
||||
}
|
||||
|
||||
const WindowFooter = styled.div`
|
||||
text-align: center;
|
||||
padding: 5px 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const FooterText = styled.span`
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 12px;
|
||||
`
|
||||
|
||||
export default Footer
|
||||
47
src/renderer/src/windows/mini/home/components/InputBar.tsx
Normal file
47
src/renderer/src/windows/mini/home/components/InputBar.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { Input as AntdInput } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface InputBarProps {
|
||||
text: string
|
||||
model: any
|
||||
referenceText: string
|
||||
placeholder: string
|
||||
handleKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void
|
||||
setText: (text: string) => void
|
||||
}
|
||||
|
||||
const InputBar: FC<InputBarProps> = ({ text, model, placeholder, handleKeyDown, setText }) => {
|
||||
const { generating } = useRuntime()
|
||||
return (
|
||||
<InputWrapper>
|
||||
<ModelAvatar model={model} size={30} />
|
||||
<Input
|
||||
value={text}
|
||||
placeholder={placeholder}
|
||||
bordered={false}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
disabled={generating}
|
||||
/>
|
||||
</InputWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const InputWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
`
|
||||
|
||||
const Input = styled(AntdInput)`
|
||||
background: none;
|
||||
border: none;
|
||||
-webkit-app-region: none;
|
||||
font-size: 18px;
|
||||
`
|
||||
|
||||
export default InputBar
|
||||
120
yarn.lock
120
yarn.lock
@ -2495,6 +2495,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ffi-napi@npm:^4":
|
||||
version: 4.0.10
|
||||
resolution: "@types/ffi-napi@npm:4.0.10"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
"@types/ref-napi": "npm:*"
|
||||
"@types/ref-struct-di": "npm:*"
|
||||
checksum: 10c0/309e4659676ea978c76ebd336935f7d7481343e982c0a6e93e5cae09ffd45530171ea1bc5832cf21a3358828e7d96d32ccb436785abb34dcbba6ecfbcc0d3f30
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/fs-extra@npm:9.0.13, @types/fs-extra@npm:^9.0.11":
|
||||
version: 9.0.13
|
||||
resolution: "@types/fs-extra@npm:9.0.13"
|
||||
@ -2724,6 +2735,24 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ref-napi@npm:*, @types/ref-napi@npm:^3":
|
||||
version: 3.0.12
|
||||
resolution: "@types/ref-napi@npm:3.0.12"
|
||||
dependencies:
|
||||
"@types/node": "npm:*"
|
||||
checksum: 10c0/d60c3e07aa908c97fdf3ea958264ca0f88d080467c045f423d7dda43d475b35e7a5798b108ddfbce7ced04a70d9787acf99991e251c6f8c0556be6e71c0e02b8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ref-struct-di@npm:*":
|
||||
version: 1.1.12
|
||||
resolution: "@types/ref-struct-di@npm:1.1.12"
|
||||
dependencies:
|
||||
"@types/ref-napi": "npm:*"
|
||||
checksum: 10c0/c52043a067fd419b30ebbfe794cad9e44606a100a31aa4d094effc121a7c30ce654dd287809f57b289f0622fb642b916a9d90b559f7f721e21c322075ce149c1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/responselike@npm:^1.0.0":
|
||||
version: 1.0.3
|
||||
resolution: "@types/responselike@npm:1.0.3"
|
||||
@ -2999,6 +3028,7 @@ __metadata:
|
||||
"@llm-tools/embedjs-openai": "npm:^0.1.25"
|
||||
"@reduxjs/toolkit": "npm:^2.2.5"
|
||||
"@types/adm-zip": "npm:^0"
|
||||
"@types/ffi-napi": "npm:^4"
|
||||
"@types/fs-extra": "npm:^11"
|
||||
"@types/lodash": "npm:^4.17.5"
|
||||
"@types/markdown-it": "npm:^14"
|
||||
@ -3006,11 +3036,13 @@ __metadata:
|
||||
"@types/react": "npm:^18.2.48"
|
||||
"@types/react-dom": "npm:^18.2.18"
|
||||
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
||||
"@types/ref-napi": "npm:^3"
|
||||
"@types/tinycolor2": "npm:^1"
|
||||
"@vitejs/plugin-react": "npm:^4.2.1"
|
||||
adm-zip: "npm:^0.5.16"
|
||||
antd: "npm:^5.22.5"
|
||||
apache-arrow: "npm:^18.1.0"
|
||||
applescript: "npm:^1.0.0"
|
||||
axios: "npm:^1.7.3"
|
||||
browser-image-compression: "npm:^2.0.2"
|
||||
dayjs: "npm:^1.11.11"
|
||||
@ -3034,6 +3066,7 @@ __metadata:
|
||||
eslint-plugin-react-hooks: "npm:^4.6.2"
|
||||
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
||||
eslint-plugin-unused-imports: "npm:^4.0.0"
|
||||
ffi-napi: "npm:^4.0.3"
|
||||
fs-extra: "npm:^11.2.0"
|
||||
html2canvas: "npm:^1.4.1"
|
||||
i18next: "npm:^23.11.5"
|
||||
@ -3055,6 +3088,7 @@ __metadata:
|
||||
react-spinners: "npm:^0.14.1"
|
||||
redux: "npm:^5.0.1"
|
||||
redux-persist: "npm:^6.0.0"
|
||||
ref-napi: "npm:^3.0.3"
|
||||
rehype-katex: "npm:^7.0.1"
|
||||
rehype-mathjax: "npm:^6.0.0"
|
||||
rehype-raw: "npm:^7.0.0"
|
||||
@ -3073,6 +3107,11 @@ __metadata:
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0
|
||||
dependenciesMeta:
|
||||
ffi-napi:
|
||||
optional: true
|
||||
ref-napi:
|
||||
optional: true
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@ -3390,6 +3429,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"applescript@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "applescript@npm:1.0.0"
|
||||
checksum: 10c0/b535e7df97a3e1272d1b8e8c832494ba3933fbad879847cb83c8990c08aed5bcb097d2af200ba2e0754c3467c2367441706b7864173e1aa9ee4132f5189287f0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"aproba@npm:^1.0.3 || ^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "aproba@npm:2.0.0"
|
||||
@ -6098,6 +6144,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ffi-napi@npm:^4.0.3":
|
||||
version: 4.0.3
|
||||
resolution: "ffi-napi@npm:4.0.3"
|
||||
dependencies:
|
||||
debug: "npm:^4.1.1"
|
||||
get-uv-event-loop-napi-h: "npm:^1.0.5"
|
||||
node-addon-api: "npm:^3.0.0"
|
||||
node-gyp: "npm:latest"
|
||||
node-gyp-build: "npm:^4.2.1"
|
||||
ref-napi: "npm:^2.0.1 || ^3.0.2"
|
||||
ref-struct-di: "npm:^1.1.0"
|
||||
checksum: 10c0/03a81bc228da1fa4f65431a3a54cf8e0367f9be45173ffd3085f13ec44b7bb9124b703535aa0b49955207f083daca86795759d6513db1605d1ed868c76d199de
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fflate@npm:0.8.1":
|
||||
version: 0.8.1
|
||||
resolution: "fflate@npm:0.8.1"
|
||||
@ -6591,6 +6652,22 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-symbol-from-current-process-h@npm:^1.0.1, get-symbol-from-current-process-h@npm:^1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "get-symbol-from-current-process-h@npm:1.0.2"
|
||||
checksum: 10c0/f33109e08aef7029b16f18032dce669e92efb21992946d7b575e494f32907c3d2c353685e00398b9202c332aab140e444e9333c93f2b81aa662bde3f59e3a25a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"get-uv-event-loop-napi-h@npm:^1.0.5":
|
||||
version: 1.0.6
|
||||
resolution: "get-uv-event-loop-napi-h@npm:1.0.6"
|
||||
dependencies:
|
||||
get-symbol-from-current-process-h: "npm:^1.0.1"
|
||||
checksum: 10c0/beb601c7e4b74fec51fb33df452a214943a1453bac9e3ca66b98fabb9ac9371f07b53d43b163a20b69228003eaf61a84b9b3faf692ed896ef66d84970411d5e0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"getpass@npm:^0.1.1":
|
||||
version: 0.1.7
|
||||
resolution: "getpass@npm:0.1.7"
|
||||
@ -9800,6 +9877,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-addon-api@npm:^3.0.0":
|
||||
version: 3.2.1
|
||||
resolution: "node-addon-api@npm:3.2.1"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/41f21c9d12318875a2c429befd06070ce367065a3ef02952cfd4ea17ef69fa14012732f510b82b226e99c254da8d671847ea018cad785f839a5366e02dd56302
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-addon-api@npm:^7.0.0":
|
||||
version: 7.1.1
|
||||
resolution: "node-addon-api@npm:7.1.1"
|
||||
@ -9848,6 +9934,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp-build@npm:^4.2.1":
|
||||
version: 4.8.4
|
||||
resolution: "node-gyp-build@npm:4.8.4"
|
||||
bin:
|
||||
node-gyp-build: bin.js
|
||||
node-gyp-build-optional: optional.js
|
||||
node-gyp-build-test: build-test.js
|
||||
checksum: 10c0/444e189907ece2081fe60e75368784f7782cfddb554b60123743dfb89509df89f1f29c03bbfa16b3a3e0be3f48799a4783f487da6203245fa5bed239ba7407e1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp@npm:8.x":
|
||||
version: 8.4.1
|
||||
resolution: "node-gyp@npm:8.4.1"
|
||||
@ -11790,6 +11887,29 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ref-napi@npm:^2.0.1 || ^3.0.2, ref-napi@npm:^3.0.3":
|
||||
version: 3.0.3
|
||||
resolution: "ref-napi@npm:3.0.3"
|
||||
dependencies:
|
||||
debug: "npm:^4.1.1"
|
||||
get-symbol-from-current-process-h: "npm:^1.0.2"
|
||||
node-addon-api: "npm:^3.0.0"
|
||||
node-gyp: "npm:latest"
|
||||
node-gyp-build: "npm:^4.2.1"
|
||||
checksum: 10c0/03768cfe6134061c4afe8c5da6cc03ced9763ad1b8401895df2c5f6b10211dfd2bbcf31cca256e2abe9f2d4ddf08333590a6589b2ec5e68efacf70de2c0e2d7b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ref-struct-di@npm:^1.1.0":
|
||||
version: 1.1.1
|
||||
resolution: "ref-struct-di@npm:1.1.1"
|
||||
dependencies:
|
||||
debug: "npm:^3.1.0"
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/e456f4b228647d06af1c911bd3f90fd85f7b9d88aa9707e10fa2879fee92d2e7a3035ca819cb30b0bdc1b32751a628308229204b00a0fb3b0e93b002dcef9835
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9":
|
||||
version: 1.0.10
|
||||
resolution: "reflect.getprototypeof@npm:1.0.10"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user