feat: add mini window
This commit is contained in:
parent
d9bb552f3f
commit
a7d9700f06
@ -50,7 +50,7 @@ export default defineConfig({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
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",
|
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||||
"@reduxjs/toolkit": "^2.2.5",
|
"@reduxjs/toolkit": "^2.2.5",
|
||||||
"@types/adm-zip": "^0",
|
"@types/adm-zip": "^0",
|
||||||
|
"@types/ffi-napi": "^4",
|
||||||
"@types/fs-extra": "^11",
|
"@types/fs-extra": "^11",
|
||||||
"@types/lodash": "^4.17.5",
|
"@types/lodash": "^4.17.5",
|
||||||
"@types/markdown-it": "^14",
|
"@types/markdown-it": "^14",
|
||||||
@ -92,9 +93,11 @@
|
|||||||
"@types/react": "^18.2.48",
|
"@types/react": "^18.2.48",
|
||||||
"@types/react-dom": "^18.2.18",
|
"@types/react-dom": "^18.2.18",
|
||||||
"@types/react-infinite-scroll-component": "^5.0.0",
|
"@types/react-infinite-scroll-component": "^5.0.0",
|
||||||
|
"@types/ref-napi": "^3",
|
||||||
"@types/tinycolor2": "^1",
|
"@types/tinycolor2": "^1",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"antd": "^5.22.5",
|
"antd": "^5.22.5",
|
||||||
|
"applescript": "^1.0.0",
|
||||||
"axios": "^1.7.3",
|
"axios": "^1.7.3",
|
||||||
"browser-image-compression": "^2.0.2",
|
"browser-image-compression": "^2.0.2",
|
||||||
"dayjs": "^1.11.11",
|
"dayjs": "^1.11.11",
|
||||||
|
|||||||
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)
|
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
|
// theme
|
||||||
ipcMain.handle('app:set-theme', (_, theme: ThemeMode) => {
|
ipcMain.handle('app:set-theme', (_, theme: ThemeMode) => {
|
||||||
configManager.setTheme(theme)
|
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)
|
this.store.set('theme', theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
isTray(): boolean {
|
getTray(): boolean {
|
||||||
return !!this.store.get('tray', true)
|
return !!this.store.get('tray', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +83,22 @@ export class ConfigManager {
|
|||||||
)
|
)
|
||||||
this.notifySubscribers('shortcuts', shortcuts)
|
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()
|
export const configManager = new ConfigManager()
|
||||||
|
|||||||
@ -3,8 +3,10 @@ import { BrowserWindow, globalShortcut } from 'electron'
|
|||||||
import Logger from 'electron-log'
|
import Logger from 'electron-log'
|
||||||
|
|
||||||
import { configManager } from './ConfigManager'
|
import { configManager } from './ConfigManager'
|
||||||
|
import { windowService } from './WindowService'
|
||||||
|
|
||||||
let showAppAccelerator: string | null = null
|
let showAppAccelerator: string | null = null
|
||||||
|
let showMiniWindowAccelerator: string | null = null
|
||||||
|
|
||||||
function getShortcutHandler(shortcut: Shortcut) {
|
function getShortcutHandler(shortcut: Shortcut) {
|
||||||
switch (shortcut.key) {
|
switch (shortcut.key) {
|
||||||
@ -26,6 +28,10 @@ function getShortcutHandler(shortcut: Shortcut) {
|
|||||||
window.focus()
|
window.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
case 'mini_window':
|
||||||
|
return () => {
|
||||||
|
windowService.toggleMiniWindow()
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -73,6 +79,10 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
showAppAccelerator = accelerator
|
showAppAccelerator = accelerator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shortcut.key === 'mini_window') {
|
||||||
|
showMiniWindowAccelerator = accelerator
|
||||||
|
}
|
||||||
|
|
||||||
if (shortcut.key.includes('zoom')) {
|
if (shortcut.key.includes('zoom')) {
|
||||||
switch (shortcut.key) {
|
switch (shortcut.key) {
|
||||||
case 'zoom_in':
|
case 'zoom_in':
|
||||||
@ -90,7 +100,7 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (shortcut.enabled) {
|
if (shortcut.enabled) {
|
||||||
globalShortcut.register(accelerator, () => handler(window))
|
globalShortcut.register(formatShortcutKey(shortcut.shortcut), () => handler(window))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
|
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)
|
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
|
||||||
handler && globalShortcut.register(showAppAccelerator, () => handler(window))
|
handler && globalShortcut.register(showAppAccelerator, () => handler(window))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showMiniWindowAccelerator) {
|
||||||
|
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
|
||||||
|
handler && globalShortcut.register(showMiniWindowAccelerator, () => handler(window))
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('[ShortcutService] Failed to unregister shortcuts')
|
Logger.error('[ShortcutService] Failed to unregister shortcuts')
|
||||||
}
|
}
|
||||||
@ -124,6 +139,7 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
export function unregisterAllShortcuts() {
|
export function unregisterAllShortcuts() {
|
||||||
try {
|
try {
|
||||||
showAppAccelerator = null
|
showAppAccelerator = null
|
||||||
|
showMiniWindowAccelerator = null
|
||||||
globalShortcut.unregisterAll()
|
globalShortcut.unregisterAll()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('[ShortcutService] Failed to unregister all shortcuts')
|
Logger.error('[ShortcutService] Failed to unregister all shortcuts')
|
||||||
|
|||||||
@ -17,6 +17,8 @@ export class TrayService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createTray() {
|
private createTray() {
|
||||||
|
this.destroyTray()
|
||||||
|
|
||||||
const iconPath = isMac ? (nativeTheme.shouldUseDarkColors ? iconLight : iconDark) : icon
|
const iconPath = isMac ? (nativeTheme.shouldUseDarkColors ? iconLight : iconDark) : icon
|
||||||
const tray = new Tray(iconPath)
|
const tray = new Tray(iconPath)
|
||||||
|
|
||||||
@ -43,6 +45,10 @@ export class TrayService {
|
|||||||
label: trayLocale.show_window,
|
label: trayLocale.show_window,
|
||||||
click: () => windowService.showMainWindow()
|
click: () => windowService.showMainWindow()
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: trayLocale.show_mini_window,
|
||||||
|
click: () => windowService.showMiniWindow()
|
||||||
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
{
|
{
|
||||||
label: trayLocale.quit,
|
label: trayLocale.quit,
|
||||||
@ -61,12 +67,17 @@ export class TrayService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.tray.on('click', () => {
|
this.tray.on('click', () => {
|
||||||
windowService.showMainWindow()
|
if (configManager.getClickTrayToShowQuickAssistant()) {
|
||||||
|
windowService.showMiniWindow()
|
||||||
|
} else {
|
||||||
|
windowService.showMainWindow()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateTray() {
|
private updateTray() {
|
||||||
if (configManager.isTray()) {
|
const showTray = configManager.getTray()
|
||||||
|
if (showTray) {
|
||||||
this.createTray()
|
this.createTray()
|
||||||
} else {
|
} else {
|
||||||
this.destroyTray()
|
this.destroyTray()
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { is } from '@electron-toolkit/utils'
|
import { is } from '@electron-toolkit/utils'
|
||||||
import { isLinux, isWin } from '@main/constant'
|
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 Logger from 'electron-log'
|
||||||
import windowStateKeeper from 'electron-window-state'
|
import windowStateKeeper from 'electron-window-state'
|
||||||
import path, { join } from 'path'
|
import path, { join } from 'path'
|
||||||
@ -13,8 +13,11 @@ import { configManager } from './ConfigManager'
|
|||||||
export class WindowService {
|
export class WindowService {
|
||||||
private static instance: WindowService | null = null
|
private static instance: WindowService | null = null
|
||||||
private mainWindow: BrowserWindow | null = null
|
private mainWindow: BrowserWindow | null = null
|
||||||
|
private miniWindow: BrowserWindow | null = null
|
||||||
private isQuitting: boolean = false
|
private isQuitting: boolean = false
|
||||||
private wasFullScreen: boolean = false
|
private wasFullScreen: boolean = false
|
||||||
|
private selectionMenuWindow: BrowserWindow | null = null
|
||||||
|
private lastSelectedText: string = ''
|
||||||
|
|
||||||
public static getInstance(): WindowService {
|
public static getInstance(): WindowService {
|
||||||
if (!WindowService.instance) {
|
if (!WindowService.instance) {
|
||||||
@ -63,6 +66,9 @@ export class WindowService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.setupMainWindow(this.mainWindow, mainWindowState)
|
this.setupMainWindow(this.mainWindow, mainWindowState)
|
||||||
|
|
||||||
|
setTimeout(() => this.showMiniWindow(), 5000)
|
||||||
|
|
||||||
return this.mainWindow
|
return this.mainWindow
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -201,7 +207,7 @@ export class WindowService {
|
|||||||
})
|
})
|
||||||
|
|
||||||
mainWindow.on('close', (event) => {
|
mainWindow.on('close', (event) => {
|
||||||
const notInTray = !configManager.isTray()
|
const notInTray = !configManager.getTray()
|
||||||
|
|
||||||
// Windows and Linux
|
// Windows and Linux
|
||||||
if ((isWin || isLinux) && notInTray) {
|
if ((isWin || isLinux) && notInTray) {
|
||||||
@ -233,6 +239,154 @@ export class WindowService {
|
|||||||
this.createMainWindow()
|
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()
|
export const windowService = WindowService.getInstance()
|
||||||
|
|||||||
@ -22,3 +22,15 @@ export function getInstanceName(baseURL: string) {
|
|||||||
return ''
|
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>
|
listFiles: (apiKey: string) => Promise<ListFilesResponse>
|
||||||
deleteFile: (apiKey: string, fileId: string) => Promise<void>
|
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),
|
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:retrieve-file', file, apiKey),
|
||||||
listFiles: (apiKey: string) => ipcRenderer.invoke('gemini:list-files', apiKey),
|
listFiles: (apiKey: string) => ipcRenderer.invoke('gemini:list-files', apiKey),
|
||||||
deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke('gemini:delete-file', apiKey, fileId)
|
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;
|
position: fixed;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#spinner img {
|
#spinner img {
|
||||||
@ -35,6 +35,7 @@
|
|||||||
<div id="spinner">
|
<div id="spinner">
|
||||||
<img src="/src/assets/images/logo.png" />
|
<img src="/src/assets/images/logo.png" />
|
||||||
</div>
|
</div>
|
||||||
|
<script type="module" src="/src/init.ts"></script>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
--color-background: var(--color-black);
|
--color-background: var(--color-black);
|
||||||
--color-background-soft: var(--color-black-soft);
|
--color-background-soft: var(--color-black-soft);
|
||||||
--color-background-mute: var(--color-black-mute);
|
--color-background-mute: var(--color-black-mute);
|
||||||
|
--color-background-opacity: rgba(34, 34, 34, 0.7);
|
||||||
|
|
||||||
--color-primary: #00b96b;
|
--color-primary: #00b96b;
|
||||||
--color-primary-soft: #00b96b99;
|
--color-primary-soft: #00b96b99;
|
||||||
@ -87,6 +88,7 @@ body[theme-mode='light'] {
|
|||||||
--color-background: var(--color-white);
|
--color-background: var(--color-white);
|
||||||
--color-background-soft: var(--color-white-soft);
|
--color-background-soft: var(--color-white-soft);
|
||||||
--color-background-mute: var(--color-white-mute);
|
--color-background-mute: var(--color-white-mute);
|
||||||
|
--color-background-opacity: rgba(255, 255, 255, 0.7);
|
||||||
|
|
||||||
--color-primary: #00b96b;
|
--color-primary: #00b96b;
|
||||||
--color-primary-soft: #00b96b99;
|
--color-primary-soft: #00b96b99;
|
||||||
|
|||||||
@ -13,7 +13,11 @@ const ThemeContext = createContext<ThemeContextType>({
|
|||||||
toggleTheme: () => {}
|
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 } = useSettings()
|
||||||
const [_theme, _setTheme] = useState(theme)
|
const [_theme, _setTheme] = useState(theme)
|
||||||
|
|
||||||
@ -22,7 +26,7 @@ export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect((): any => {
|
useEffect((): any => {
|
||||||
if (theme === ThemeMode.auto) {
|
if (theme === ThemeMode.auto || defaultTheme === ThemeMode.auto) {
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||||
_setTheme(mediaQuery.matches ? ThemeMode.dark : ThemeMode.light)
|
_setTheme(mediaQuery.matches ? ThemeMode.dark : ThemeMode.light)
|
||||||
const handleChange = (e: MediaQueryListEvent) => _setTheme(e.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 {
|
} else {
|
||||||
_setTheme(theme)
|
_setTheme(theme)
|
||||||
}
|
}
|
||||||
}, [theme])
|
}, [defaultTheme, theme])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.body.setAttribute('theme-mode', _theme)
|
document.body.setAttribute('theme-mode', _theme)
|
||||||
|
|||||||
@ -9,7 +9,10 @@ export default function useUpdateHandler() {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!window.electron) return
|
||||||
|
|
||||||
const ipcRenderer = window.electron.ipcRenderer
|
const ipcRenderer = window.electron.ipcRenderer
|
||||||
|
|
||||||
const removers = [
|
const removers = [
|
||||||
ipcRenderer.on('update-not-available', () => {
|
ipcRenderer.on('update-not-available', () => {
|
||||||
dispatch(setUpdateState({ checking: false }))
|
dispatch(setUpdateState({ checking: false }))
|
||||||
|
|||||||
@ -385,6 +385,10 @@
|
|||||||
"webdav.syncError": "Backup Error",
|
"webdav.syncError": "Backup Error",
|
||||||
"webdav.lastSync": "Last Backup"
|
"webdav.lastSync": "Last Backup"
|
||||||
},
|
},
|
||||||
|
"quickAssistant": {
|
||||||
|
"title": "Quick Assistant",
|
||||||
|
"click_tray_to_show": "Click the system tray icon to open"
|
||||||
|
},
|
||||||
"display.title": "Display Settings",
|
"display.title": "Display Settings",
|
||||||
"font_size.title": "Message font size",
|
"font_size.title": "Message font size",
|
||||||
"general": "General Settings",
|
"general": "General Settings",
|
||||||
@ -519,7 +523,8 @@
|
|||||||
"toggle_show_assistants": "Toggle Assistants",
|
"toggle_show_assistants": "Toggle Assistants",
|
||||||
"toggle_show_topics": "Toggle Topics",
|
"toggle_show_topics": "Toggle Topics",
|
||||||
"copy_last_message": "Copy Last Message",
|
"copy_last_message": "Copy Last Message",
|
||||||
"search_message": "Search Message"
|
"search_message": "Search Message",
|
||||||
|
"mini_window": "Quick Assistant"
|
||||||
},
|
},
|
||||||
"theme.auto": "Auto",
|
"theme.auto": "Auto",
|
||||||
"theme.dark": "Dark",
|
"theme.dark": "Dark",
|
||||||
@ -552,7 +557,8 @@
|
|||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
"quit": "Quit",
|
"quit": "Quit",
|
||||||
"show_window": "Show Window"
|
"show_window": "Show Window",
|
||||||
|
"show_mini_window": "Quick Assistant"
|
||||||
},
|
},
|
||||||
"words": {
|
"words": {
|
||||||
"knowledgeGraph": "Knowledge Graph",
|
"knowledgeGraph": "Knowledge Graph",
|
||||||
@ -634,7 +640,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prompts": {
|
"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.syncError": "バックアップエラー",
|
||||||
"webdav.lastSync": "最終同期"
|
"webdav.lastSync": "最終同期"
|
||||||
},
|
},
|
||||||
|
"quickAssistant": {
|
||||||
|
"title": "クイックアシスタント",
|
||||||
|
"click_tray_to_show": "システムトレイアイコンをクリックして開く"
|
||||||
|
},
|
||||||
"display.title": "表示設定",
|
"display.title": "表示設定",
|
||||||
"font_size.title": "メッセージのフォントサイズ",
|
"font_size.title": "メッセージのフォントサイズ",
|
||||||
"general": "一般設定",
|
"general": "一般設定",
|
||||||
@ -504,7 +508,8 @@
|
|||||||
"toggle_show_assistants": "アシスタントの表示を切り替え",
|
"toggle_show_assistants": "アシスタントの表示を切り替え",
|
||||||
"toggle_show_topics": "トピックの表示を切り替え",
|
"toggle_show_topics": "トピックの表示を切り替え",
|
||||||
"copy_last_message": "最後のメッセージをコピー",
|
"copy_last_message": "最後のメッセージをコピー",
|
||||||
"search_message": "メッセージを検索"
|
"search_message": "メッセージを検索",
|
||||||
|
"mini_window": "クイックアシスタント"
|
||||||
},
|
},
|
||||||
"theme.auto": "自動",
|
"theme.auto": "自動",
|
||||||
"theme.dark": "ダークテーマ",
|
"theme.dark": "ダークテーマ",
|
||||||
@ -537,7 +542,8 @@
|
|||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
"quit": "終了",
|
"quit": "終了",
|
||||||
"show_window": "ウィンドウを表示"
|
"show_window": "ウィンドウを表示",
|
||||||
|
"show_mini_window": "クイックアシスタント"
|
||||||
},
|
},
|
||||||
"words": {
|
"words": {
|
||||||
"knowledgeGraph": "ナレッジグラフ",
|
"knowledgeGraph": "ナレッジグラフ",
|
||||||
@ -619,7 +625,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prompts": {
|
"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.syncError": "Ошибка резервного копирования",
|
||||||
"webdav.lastSync": "Последняя синхронизация"
|
"webdav.lastSync": "Последняя синхронизация"
|
||||||
},
|
},
|
||||||
|
"quickAssistant": {
|
||||||
|
"title": "Быстрый помощник",
|
||||||
|
"click_tray_to_show": "Нажмите на иконку системного трея для открытия"
|
||||||
|
},
|
||||||
"display.title": "Настройки отображения",
|
"display.title": "Настройки отображения",
|
||||||
"font_size.title": "Размер шрифта сообщений",
|
"font_size.title": "Размер шрифта сообщений",
|
||||||
"general": "Общие настройки",
|
"general": "Общие настройки",
|
||||||
@ -518,7 +522,8 @@
|
|||||||
"toggle_show_assistants": "Переключить отображение ассистентов",
|
"toggle_show_assistants": "Переключить отображение ассистентов",
|
||||||
"toggle_show_topics": "Переключить отображение топиков",
|
"toggle_show_topics": "Переключить отображение топиков",
|
||||||
"copy_last_message": "Копировать последнее сообщение",
|
"copy_last_message": "Копировать последнее сообщение",
|
||||||
"search_message": "Поиск сообщения"
|
"search_message": "Поиск сообщения",
|
||||||
|
"mini_window": "Быстрый помощник"
|
||||||
},
|
},
|
||||||
"theme.auto": "Автоматически",
|
"theme.auto": "Автоматически",
|
||||||
"theme.dark": "Темная",
|
"theme.dark": "Темная",
|
||||||
@ -551,7 +556,8 @@
|
|||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
"quit": "Выйти",
|
"quit": "Выйти",
|
||||||
"show_window": "Показать окно"
|
"show_window": "Показать окно",
|
||||||
|
"show_mini_window": "Быстрый помощник"
|
||||||
},
|
},
|
||||||
"words": {
|
"words": {
|
||||||
"knowledgeGraph": "Граф знаний",
|
"knowledgeGraph": "Граф знаний",
|
||||||
@ -633,7 +639,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prompts": {
|
"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.syncError": "备份错误",
|
||||||
"webdav.lastSync": "上次备份时间"
|
"webdav.lastSync": "上次备份时间"
|
||||||
},
|
},
|
||||||
|
"quickAssistant": {
|
||||||
|
"title": "快捷助手",
|
||||||
|
"click_tray_to_show": "点击系统托盘图标打开"
|
||||||
|
},
|
||||||
"display.title": "显示设置",
|
"display.title": "显示设置",
|
||||||
"font_size.title": "消息字体大小",
|
"font_size.title": "消息字体大小",
|
||||||
"general": "常规设置",
|
"general": "常规设置",
|
||||||
@ -507,7 +511,8 @@
|
|||||||
"toggle_show_assistants": "切换助手显示",
|
"toggle_show_assistants": "切换助手显示",
|
||||||
"toggle_show_topics": "切换话题显示",
|
"toggle_show_topics": "切换话题显示",
|
||||||
"copy_last_message": "复制上一条消息",
|
"copy_last_message": "复制上一条消息",
|
||||||
"search_message": "搜索消息"
|
"search_message": "搜索消息",
|
||||||
|
"mini_window": "快捷助手"
|
||||||
},
|
},
|
||||||
"theme.auto": "跟随系统",
|
"theme.auto": "跟随系统",
|
||||||
"theme.dark": "深色主题",
|
"theme.dark": "深色主题",
|
||||||
@ -540,7 +545,8 @@
|
|||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
"quit": "退出",
|
"quit": "退出",
|
||||||
"show_window": "显示窗口"
|
"show_window": "显示窗口",
|
||||||
|
"show_mini_window": "快捷助手"
|
||||||
},
|
},
|
||||||
"words": {
|
"words": {
|
||||||
"knowledgeGraph": "知识图谱",
|
"knowledgeGraph": "知识图谱",
|
||||||
@ -622,7 +628,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prompts": {
|
"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.syncError": "備份錯誤",
|
||||||
"webdav.lastSync": "上次同步時間"
|
"webdav.lastSync": "上次同步時間"
|
||||||
},
|
},
|
||||||
|
"quickAssistant": {
|
||||||
|
"title": "快捷助手",
|
||||||
|
"click_tray_to_show": "點擊系統托盤圖標打開"
|
||||||
|
},
|
||||||
"display.title": "顯示設定",
|
"display.title": "顯示設定",
|
||||||
"font_size.title": "訊息字體大小",
|
"font_size.title": "訊息字體大小",
|
||||||
"general": "一般設定",
|
"general": "一般設定",
|
||||||
@ -506,7 +510,8 @@
|
|||||||
"toggle_show_assistants": "切換助手顯示",
|
"toggle_show_assistants": "切換助手顯示",
|
||||||
"toggle_show_topics": "切換話題顯示",
|
"toggle_show_topics": "切換話題顯示",
|
||||||
"copy_last_message": "複製上一条消息",
|
"copy_last_message": "複製上一条消息",
|
||||||
"search_message": "搜索消息"
|
"search_message": "搜索消息",
|
||||||
|
"mini_window": "快捷助手"
|
||||||
},
|
},
|
||||||
"theme.auto": "自動",
|
"theme.auto": "自動",
|
||||||
"theme.dark": "深色主題",
|
"theme.dark": "深色主題",
|
||||||
@ -539,7 +544,8 @@
|
|||||||
},
|
},
|
||||||
"tray": {
|
"tray": {
|
||||||
"quit": "退出",
|
"quit": "退出",
|
||||||
"show_window": "顯示視窗"
|
"show_window": "顯示視窗",
|
||||||
|
"show_mini_window": "快捷助手"
|
||||||
},
|
},
|
||||||
"words": {
|
"words": {
|
||||||
"knowledgeGraph": "知識圖譜",
|
"knowledgeGraph": "知識圖譜",
|
||||||
@ -621,7 +627,31 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prompts": {
|
"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 { startAutoSync } from './services/BackupService'
|
||||||
import store from './store'
|
import store from './store'
|
||||||
|
|
||||||
|
function initSpinner() {
|
||||||
|
const spinner = document.getElementById('spinner')
|
||||||
|
if (spinner && window.location.hash !== '#/mini') {
|
||||||
|
spinner.style.display = 'flex'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function initKeyv() {
|
function initKeyv() {
|
||||||
window.keyv = new KeyvStorage()
|
window.keyv = new KeyvStorage()
|
||||||
window.keyv.init()
|
window.keyv.init()
|
||||||
@ -17,5 +24,6 @@ function initAutoSync() {
|
|||||||
}, 2000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initSpinner()
|
||||||
initKeyv()
|
initKeyv()
|
||||||
initAutoSync()
|
initAutoSync()
|
||||||
|
|||||||
@ -1,8 +1,13 @@
|
|||||||
import './assets/styles/index.scss'
|
import './assets/styles/index.scss'
|
||||||
import './init'
|
|
||||||
|
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
|
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import MiniApp from './windows/mini/App'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<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)
|
messages.findIndex((m) => m.id === message.id)
|
||||||
),
|
),
|
||||||
assistant: assistantWithModel,
|
assistant: assistantWithModel,
|
||||||
topic,
|
|
||||||
onResponse: (msg) => {
|
onResponse: (msg) => {
|
||||||
setMessage(msg)
|
setMessage(msg)
|
||||||
if (msg.status !== 'pending') {
|
if (msg.status !== 'pending') {
|
||||||
|
|||||||
@ -52,7 +52,7 @@ const WebDavSettings: FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
setBackuping(true)
|
setBackuping(true)
|
||||||
await backupToWebdav()
|
await backupToWebdav({ showMessage: true })
|
||||||
setBackuping(false)
|
setBackuping(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,9 +56,9 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
<div style={{ marginBottom: 10 }}>{t('settings.models.topic_naming_prompt')}</div>
|
<div style={{ marginBottom: 10 }}>{t('settings.models.topic_naming_prompt')}</div>
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
rows={4}
|
rows={4}
|
||||||
value={topicNamingPrompt || t('prompts.summarize')}
|
value={topicNamingPrompt || t('prompts.title')}
|
||||||
onChange={(e) => dispatch(setTopicNamingPrompt(e.target.value.trim()))}
|
onChange={(e) => dispatch(setTopicNamingPrompt(e.target.value.trim()))}
|
||||||
placeholder={t('prompts.summarize')}
|
placeholder={t('prompts.title')}
|
||||||
/>
|
/>
|
||||||
{topicNamingPrompt && (
|
{topicNamingPrompt && (
|
||||||
<Button style={{ marginTop: 10 }} onClick={handleReset}>
|
<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,
|
InfoCircleOutlined,
|
||||||
LayoutOutlined,
|
LayoutOutlined,
|
||||||
MacCommandOutlined,
|
MacCommandOutlined,
|
||||||
|
RocketOutlined,
|
||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
SettingOutlined
|
SettingOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
@ -19,6 +20,7 @@ import DisplaySettings from './DisplaySettings/DisplaySettings'
|
|||||||
import GeneralSettings from './GeneralSettings'
|
import GeneralSettings from './GeneralSettings'
|
||||||
import ModelSettings from './ModalSettings/ModelSettings'
|
import ModelSettings from './ModalSettings/ModelSettings'
|
||||||
import ProvidersList from './ProviderSettings'
|
import ProvidersList from './ProviderSettings'
|
||||||
|
import QuickAssistantSettings from './QuickAssistantSettings'
|
||||||
import ShortcutSettings from './ShortcutSettings'
|
import ShortcutSettings from './ShortcutSettings'
|
||||||
|
|
||||||
const SettingsPage: FC = () => {
|
const SettingsPage: FC = () => {
|
||||||
@ -68,6 +70,12 @@ const SettingsPage: FC = () => {
|
|||||||
{t('settings.shortcuts.title')}
|
{t('settings.shortcuts.title')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</MenuItemLink>
|
</MenuItemLink>
|
||||||
|
<MenuItemLink to="/settings/quickAssistant">
|
||||||
|
<MenuItem className={isRoute('/settings/quickAssistant')}>
|
||||||
|
<RocketOutlined />
|
||||||
|
{t('settings.quickAssistant.title')}
|
||||||
|
</MenuItem>
|
||||||
|
</MenuItemLink>
|
||||||
<MenuItemLink to="/settings/data">
|
<MenuItemLink to="/settings/data">
|
||||||
<MenuItem className={isRoute('/settings/data')}>
|
<MenuItem className={isRoute('/settings/data')}>
|
||||||
<SaveOutlined />
|
<SaveOutlined />
|
||||||
@ -88,6 +96,7 @@ const SettingsPage: FC = () => {
|
|||||||
<Route path="general/*" element={<GeneralSettings />} />
|
<Route path="general/*" element={<GeneralSettings />} />
|
||||||
<Route path="display" element={<DisplaySettings />} />
|
<Route path="display" element={<DisplaySettings />} />
|
||||||
<Route path="data/*" element={<DataSettings />} />
|
<Route path="data/*" element={<DataSettings />} />
|
||||||
|
<Route path="quickAssistant" element={<QuickAssistantSettings />} />
|
||||||
<Route path="shortcut" element={<ShortcutSettings />} />
|
<Route path="shortcut" element={<ShortcutSettings />} />
|
||||||
<Route path="about" element={<AboutSettings />} />
|
<Route path="about" element={<AboutSettings />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@ -190,7 +190,7 @@ export default class AnthropicProvider extends BaseProvider {
|
|||||||
|
|
||||||
const systemMessage = {
|
const systemMessage = {
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.summarize')
|
content: (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title')
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
|
|||||||
@ -269,7 +269,7 @@ export default class GeminiProvider extends BaseProvider {
|
|||||||
|
|
||||||
const systemMessage = {
|
const systemMessage = {
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.summarize')
|
content: (getStoreSetting('topicNamingPrompt') as string) || i18n.t('prompts.title')
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
|
|||||||
@ -322,7 +322,7 @@ export default class OpenAIProvider extends BaseProvider {
|
|||||||
|
|
||||||
const systemMessage = {
|
const systemMessage = {
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: getStoreSetting('topicNamingPrompt') || i18n.t('prompts.summarize')
|
content: getStoreSetting('topicNamingPrompt') || i18n.t('prompts.title')
|
||||||
}
|
}
|
||||||
|
|
||||||
const userMessage = {
|
const userMessage = {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { setGenerating } from '@renderer/store/runtime'
|
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 { isEmpty } from 'lodash'
|
||||||
|
|
||||||
import AiProvider from '../providers/AiProvider'
|
import AiProvider from '../providers/AiProvider'
|
||||||
@ -24,7 +24,6 @@ export async function fetchChatCompletion({
|
|||||||
}: {
|
}: {
|
||||||
message: Message
|
message: Message
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
topic: Topic
|
|
||||||
assistant: Assistant
|
assistant: Assistant
|
||||||
onResponse: (message: Message) => void
|
onResponse: (message: Message) => void
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@ -181,10 +181,8 @@ export function startAutoSync() {
|
|||||||
try {
|
try {
|
||||||
console.log('[AutoSync] Performing auto backup...')
|
console.log('[AutoSync] Performing auto backup...')
|
||||||
await backupToWebdav({ showMessage: false })
|
await backupToWebdav({ showMessage: false })
|
||||||
window.message.success({ content: i18n.t('message.backup.success'), key: 'webdav-auto-sync' })
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[AutoSync] Auto backup failed:', error)
|
console.error('[AutoSync] Auto backup failed:', error)
|
||||||
window.message.error({ content: i18n.t('message.backup.failed'), key: 'webdav-auto-sync' })
|
|
||||||
} finally {
|
} finally {
|
||||||
isAutoBackupRunning = false
|
isAutoBackupRunning = false
|
||||||
scheduleNextBackup()
|
scheduleNextBackup()
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 56,
|
version: 57,
|
||||||
blacklist: ['runtime'],
|
blacklist: ['runtime'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -407,6 +407,7 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setDefaultModel: (state, action: PayloadAction<{ model: Model }>) => {
|
setDefaultModel: (state, action: PayloadAction<{ model: Model }>) => {
|
||||||
state.defaultModel = action.payload.model
|
state.defaultModel = action.payload.model
|
||||||
|
window.electron.ipcRenderer.send('miniwindow-reload')
|
||||||
},
|
},
|
||||||
setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => {
|
setTopicNamingModel: (state, action: PayloadAction<{ model: Model }>) => {
|
||||||
state.topicNamingModel = action.payload.model
|
state.topicNamingModel = action.payload.model
|
||||||
|
|||||||
@ -812,6 +812,16 @@ const migrateConfig = {
|
|||||||
enabled: false
|
enabled: false
|
||||||
})
|
})
|
||||||
return state
|
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[]
|
disabled: SidebarIcon[]
|
||||||
}
|
}
|
||||||
narrowMode: boolean
|
narrowMode: boolean
|
||||||
|
clickTrayToShowQuickAssistant: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SettingsState = {
|
const initialState: SettingsState = {
|
||||||
@ -105,7 +106,8 @@ const initialState: SettingsState = {
|
|||||||
visible: DEFAULT_SIDEBAR_ICONS,
|
visible: DEFAULT_SIDEBAR_ICONS,
|
||||||
disabled: []
|
disabled: []
|
||||||
},
|
},
|
||||||
narrowMode: false
|
narrowMode: false,
|
||||||
|
clickTrayToShowQuickAssistant: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
const settingsSlice = createSlice({
|
||||||
@ -240,6 +242,9 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setNarrowMode: (state, action: PayloadAction<boolean>) => {
|
setNarrowMode: (state, action: PayloadAction<boolean>) => {
|
||||||
state.narrowMode = action.payload
|
state.narrowMode = action.payload
|
||||||
|
},
|
||||||
|
setClickTrayToShowQuickAssistant: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.clickTrayToShowQuickAssistant = action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -285,7 +290,8 @@ export const {
|
|||||||
setCustomCss,
|
setCustomCss,
|
||||||
setTopicNamingPrompt,
|
setTopicNamingPrompt,
|
||||||
setSidebarIcons,
|
setSidebarIcons,
|
||||||
setNarrowMode
|
setNarrowMode,
|
||||||
|
setClickTrayToShowQuickAssistant
|
||||||
} = settingsSlice.actions
|
} = settingsSlice.actions
|
||||||
|
|
||||||
export default settingsSlice.reducer
|
export default settingsSlice.reducer
|
||||||
|
|||||||
@ -51,6 +51,13 @@ const initialState: ShortcutsState = {
|
|||||||
editable: true,
|
editable: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
system: false
|
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
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/fs-extra@npm:9.0.13, @types/fs-extra@npm:^9.0.11":
|
||||||
version: 9.0.13
|
version: 9.0.13
|
||||||
resolution: "@types/fs-extra@npm:9.0.13"
|
resolution: "@types/fs-extra@npm:9.0.13"
|
||||||
@ -2724,6 +2735,24 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/responselike@npm:^1.0.0":
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
resolution: "@types/responselike@npm:1.0.3"
|
resolution: "@types/responselike@npm:1.0.3"
|
||||||
@ -2999,6 +3028,7 @@ __metadata:
|
|||||||
"@llm-tools/embedjs-openai": "npm:^0.1.25"
|
"@llm-tools/embedjs-openai": "npm:^0.1.25"
|
||||||
"@reduxjs/toolkit": "npm:^2.2.5"
|
"@reduxjs/toolkit": "npm:^2.2.5"
|
||||||
"@types/adm-zip": "npm:^0"
|
"@types/adm-zip": "npm:^0"
|
||||||
|
"@types/ffi-napi": "npm:^4"
|
||||||
"@types/fs-extra": "npm:^11"
|
"@types/fs-extra": "npm:^11"
|
||||||
"@types/lodash": "npm:^4.17.5"
|
"@types/lodash": "npm:^4.17.5"
|
||||||
"@types/markdown-it": "npm:^14"
|
"@types/markdown-it": "npm:^14"
|
||||||
@ -3006,11 +3036,13 @@ __metadata:
|
|||||||
"@types/react": "npm:^18.2.48"
|
"@types/react": "npm:^18.2.48"
|
||||||
"@types/react-dom": "npm:^18.2.18"
|
"@types/react-dom": "npm:^18.2.18"
|
||||||
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
||||||
|
"@types/ref-napi": "npm:^3"
|
||||||
"@types/tinycolor2": "npm:^1"
|
"@types/tinycolor2": "npm:^1"
|
||||||
"@vitejs/plugin-react": "npm:^4.2.1"
|
"@vitejs/plugin-react": "npm:^4.2.1"
|
||||||
adm-zip: "npm:^0.5.16"
|
adm-zip: "npm:^0.5.16"
|
||||||
antd: "npm:^5.22.5"
|
antd: "npm:^5.22.5"
|
||||||
apache-arrow: "npm:^18.1.0"
|
apache-arrow: "npm:^18.1.0"
|
||||||
|
applescript: "npm:^1.0.0"
|
||||||
axios: "npm:^1.7.3"
|
axios: "npm:^1.7.3"
|
||||||
browser-image-compression: "npm:^2.0.2"
|
browser-image-compression: "npm:^2.0.2"
|
||||||
dayjs: "npm:^1.11.11"
|
dayjs: "npm:^1.11.11"
|
||||||
@ -3034,6 +3066,7 @@ __metadata:
|
|||||||
eslint-plugin-react-hooks: "npm:^4.6.2"
|
eslint-plugin-react-hooks: "npm:^4.6.2"
|
||||||
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
||||||
eslint-plugin-unused-imports: "npm:^4.0.0"
|
eslint-plugin-unused-imports: "npm:^4.0.0"
|
||||||
|
ffi-napi: "npm:^4.0.3"
|
||||||
fs-extra: "npm:^11.2.0"
|
fs-extra: "npm:^11.2.0"
|
||||||
html2canvas: "npm:^1.4.1"
|
html2canvas: "npm:^1.4.1"
|
||||||
i18next: "npm:^23.11.5"
|
i18next: "npm:^23.11.5"
|
||||||
@ -3055,6 +3088,7 @@ __metadata:
|
|||||||
react-spinners: "npm:^0.14.1"
|
react-spinners: "npm:^0.14.1"
|
||||||
redux: "npm:^5.0.1"
|
redux: "npm:^5.0.1"
|
||||||
redux-persist: "npm:^6.0.0"
|
redux-persist: "npm:^6.0.0"
|
||||||
|
ref-napi: "npm:^3.0.3"
|
||||||
rehype-katex: "npm:^7.0.1"
|
rehype-katex: "npm:^7.0.1"
|
||||||
rehype-mathjax: "npm:^6.0.0"
|
rehype-mathjax: "npm:^6.0.0"
|
||||||
rehype-raw: "npm:^7.0.0"
|
rehype-raw: "npm:^7.0.0"
|
||||||
@ -3073,6 +3107,11 @@ __metadata:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^17.0.0 || ^18.0.0
|
react: ^17.0.0 || ^18.0.0
|
||||||
react-dom: ^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
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@ -3390,6 +3429,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"aproba@npm:^1.0.3 || ^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "aproba@npm:2.0.0"
|
resolution: "aproba@npm:2.0.0"
|
||||||
@ -6098,6 +6144,21 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"fflate@npm:0.8.1":
|
||||||
version: 0.8.1
|
version: 0.8.1
|
||||||
resolution: "fflate@npm:0.8.1"
|
resolution: "fflate@npm:0.8.1"
|
||||||
@ -6591,6 +6652,22 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"getpass@npm:^0.1.1":
|
||||||
version: 0.1.7
|
version: 0.1.7
|
||||||
resolution: "getpass@npm:0.1.7"
|
resolution: "getpass@npm:0.1.7"
|
||||||
@ -9800,6 +9877,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"node-addon-api@npm:^7.0.0":
|
||||||
version: 7.1.1
|
version: 7.1.1
|
||||||
resolution: "node-addon-api@npm:7.1.1"
|
resolution: "node-addon-api@npm:7.1.1"
|
||||||
@ -9848,6 +9934,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"node-gyp@npm:8.x":
|
||||||
version: 8.4.1
|
version: 8.4.1
|
||||||
resolution: "node-gyp@npm:8.4.1"
|
resolution: "node-gyp@npm:8.4.1"
|
||||||
@ -11790,6 +11887,29 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"reflect.getprototypeof@npm:^1.0.6, reflect.getprototypeof@npm:^1.0.9":
|
||||||
version: 1.0.10
|
version: 1.0.10
|
||||||
resolution: "reflect.getprototypeof@npm:1.0.10"
|
resolution: "reflect.getprototypeof@npm:1.0.10"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user