Compare commits
1 Commits
v0.9.14
...
feat/plugi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebf61b1ce9 |
19
.github/workflows/release.yml
vendored
@@ -78,10 +78,19 @@ jobs:
|
||||
run: node scripts/replace-spaces.js
|
||||
|
||||
- name: Release
|
||||
uses: ncipollo/release-action@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
draft: true
|
||||
allowUpdates: true
|
||||
makeLatest: false
|
||||
artifacts: 'dist/*.exe,dist/*.zip,dist/*.dmg,dist/*.AppImage,dist/*.snap,dist/*.deb,dist/*.rpm,dist/*.tar.gz,dist/latest*.yml,dist/*.blockmap'
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
files: |
|
||||
dist/*.exe
|
||||
dist/*.zip
|
||||
dist/*.dmg
|
||||
dist/*.AppImage
|
||||
dist/*.snap
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
dist/*.tar.gz
|
||||
dist/latest*.yml
|
||||
dist/*.blockmap
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
diff --git a/src/markdown-loader.js b/src/markdown-loader.js
|
||||
index 8a17cb7f5a68d90d2be21682db6e95ce22a3e71c..9ee868ef9d4ff3dc914b3abc3c8006deb1e9c6c6 100644
|
||||
--- a/src/markdown-loader.js
|
||||
+++ b/src/markdown-loader.js
|
||||
@@ -1,5 +1,4 @@
|
||||
import { micromark } from 'micromark';
|
||||
-import { mdxJsx } from 'micromark-extension-mdx-jsx';
|
||||
import { gfmHtml, gfm } from 'micromark-extension-gfm';
|
||||
import createDebugMessages from 'debug';
|
||||
import fs from 'node:fs';
|
||||
@@ -21,7 +20,7 @@ export class MarkdownLoader extends BaseLoader {
|
||||
? (await getSafe(this.filePathOrUrl, { format: 'buffer' })).body
|
||||
: await stream2buffer(fs.createReadStream(this.filePathOrUrl));
|
||||
this.debug('MarkdownLoader stream created');
|
||||
- const result = micromark(buffer, { extensions: [gfm(), mdxJsx()], htmlExtensions: [gfmHtml()] });
|
||||
+ const result = micromark(buffer, { extensions: [gfm()], htmlExtensions: [gfmHtml()] });
|
||||
this.debug('Markdown parsed...');
|
||||
const webLoader = new WebLoader({
|
||||
urlOrContent: result,
|
||||
BIN
build/icon.icns
BIN
build/icon.ico
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 353 KiB |
BIN
build/icon.png
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 210 KiB |
BIN
build/logo.png
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 5.9 KiB |
@@ -32,10 +32,6 @@ asarUnpack:
|
||||
- '**/*.{node,dll,metal,exp,lib}'
|
||||
win:
|
||||
executableName: Cherry Studio
|
||||
artifactName: ${productName}-${version}-portable.${ext}
|
||||
target:
|
||||
- target: nsis
|
||||
- target: portable
|
||||
nsis:
|
||||
artifactName: ${productName}-${version}-setup.${ext}
|
||||
shortcutName: ${productName}
|
||||
@@ -47,7 +43,6 @@ nsis:
|
||||
mac:
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
notarize: false
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
@@ -62,8 +57,9 @@ mac:
|
||||
arch:
|
||||
- arm64
|
||||
- x64
|
||||
linux:
|
||||
dmg:
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
linux:
|
||||
target:
|
||||
- target: AppImage
|
||||
arch:
|
||||
@@ -71,6 +67,8 @@ linux:
|
||||
- x64
|
||||
maintainer: electronjs.org
|
||||
category: Utility
|
||||
appImage:
|
||||
artifactName: ${productName}-${version}-${arch}.${ext}
|
||||
publish:
|
||||
provider: generic
|
||||
url: https://cherrystudio.ocool.online
|
||||
@@ -80,11 +78,4 @@ afterPack: scripts/after-pack.js
|
||||
afterSign: scripts/notarize.js
|
||||
releaseInfo:
|
||||
releaseNotes: |
|
||||
新增快捷助手弹窗
|
||||
翻译默认使用流输出
|
||||
小程序弹窗顶部增加固定按钮 @ousugo
|
||||
新增清除消息、清除上下文快捷键 @cljnnn
|
||||
Gemini 安全设置更新 @magicdmer
|
||||
智能体页面性能优化 @magicdmer
|
||||
修复 WebDAV 不能自动备份问题
|
||||
⚠️ 如果不能自动更新,请手动下载安装包
|
||||
增加 Genspark 小程序
|
||||
|
||||
@@ -50,7 +50,7 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['chunk-RK3FTE5R.js']
|
||||
exclude: ['chunk-QH6N6I7P.js', 'chunk-PB73W2YU.js']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "CherryStudio",
|
||||
"version": "0.9.14",
|
||||
"version": "0.9.2",
|
||||
"private": true,
|
||||
"description": "A powerful AI assistant for producer.",
|
||||
"main": "./out/main/index.js",
|
||||
@@ -50,11 +50,10 @@
|
||||
"@electron-toolkit/preload": "^3.0.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@electron/notarize": "^2.5.0",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@llm-tools/embedjs": "patch:@llm-tools/embedjs@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-npm-0.1.25-ec5645cf36.patch",
|
||||
"@llm-tools/embedjs-libsql": "patch:@llm-tools/embedjs-libsql@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-libsql-npm-0.1.25-fad000d74c.patch",
|
||||
"@llm-tools/embedjs-loader-csv": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-markdown": "patch:@llm-tools/embedjs-loader-markdown@npm%3A0.1.25#~/.yarn/patches/@llm-tools-embedjs-loader-markdown-npm-0.1.25-d1d536d640.patch",
|
||||
"@llm-tools/embedjs-loader-markdown": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-msoffice": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-pdf": "^0.1.25",
|
||||
"@llm-tools/embedjs-loader-sitemap": "^0.1.25",
|
||||
@@ -81,6 +80,7 @@
|
||||
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^1.0.1",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@hello-pangea/dnd": "^16.6.0",
|
||||
"@kangfenmao/keyv-storage": "^0.1.0",
|
||||
"@reduxjs/toolkit": "^2.2.5",
|
||||
@@ -95,8 +95,7 @@
|
||||
"@types/tinycolor2": "^1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"antd": "^5.22.5",
|
||||
"applescript": "^1.0.0",
|
||||
"axios": "^1.7.3",
|
||||
"axios": "^1.7.9",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"dayjs": "^1.11.11",
|
||||
"dexie": "^4.0.8",
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
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()
|
||||
@@ -1,7 +1,9 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import vm from 'node:vm'
|
||||
|
||||
import { Shortcut, ThemeMode } from '@types'
|
||||
import axios from 'axios'
|
||||
import { BrowserWindow, ipcMain, ProxyConfig, session, shell } from 'electron'
|
||||
import log from 'electron-log'
|
||||
|
||||
@@ -11,10 +13,8 @@ import BackupManager from './services/BackupManager'
|
||||
import { configManager } from './services/ConfigManager'
|
||||
import { ExportService } from './services/ExportService'
|
||||
import FileStorage from './services/FileStorage'
|
||||
import { GeminiService } from './services/GeminiService'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
import { TrayService } from './services/TrayService'
|
||||
import { windowService } from './services/WindowService'
|
||||
import { compress, decompress } from './utils/zip'
|
||||
|
||||
@@ -53,16 +53,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
configManager.setTray(isActive)
|
||||
})
|
||||
|
||||
ipcMain.handle('app:restart-tray', () => TrayService.getInstance().restartTray())
|
||||
|
||||
ipcMain.handle('config:set', (_, key: string, value: any) => {
|
||||
configManager.set(key, value)
|
||||
})
|
||||
|
||||
ipcMain.handle('config:get', (_, key: string) => {
|
||||
return configManager.get(key)
|
||||
})
|
||||
|
||||
// theme
|
||||
ipcMain.handle('app:set-theme', (_, theme: ThemeMode) => {
|
||||
configManager.setTheme(theme)
|
||||
@@ -128,7 +118,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('file:base64Image', fileManager.base64Image)
|
||||
ipcMain.handle('file:download', fileManager.downloadFile)
|
||||
ipcMain.handle('file:copy', fileManager.copyFile)
|
||||
ipcMain.handle('file:binaryFile', fileManager.binaryFile)
|
||||
|
||||
// minapp
|
||||
ipcMain.handle('minapp', (_, args) => {
|
||||
@@ -168,29 +157,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('knowledge-base:remove', KnowledgeService.remove)
|
||||
ipcMain.handle('knowledge-base:search', KnowledgeService.search)
|
||||
|
||||
// window
|
||||
ipcMain.handle('window:set-minimum-size', (_, width: number, height: number) => {
|
||||
mainWindow?.setMinimumSize(width, height)
|
||||
// vm
|
||||
ipcMain.handle('run-js', (_, code: string) => {
|
||||
const context = vm.createContext(Object.assign({ fetch: fetch, URL: URL, axios: axios }, global))
|
||||
return vm.runInContext(code, context)
|
||||
})
|
||||
|
||||
ipcMain.handle('window:reset-minimum-size', () => {
|
||||
mainWindow?.setMinimumSize(1080, 600)
|
||||
const [width, height] = mainWindow?.getSize() ?? [1080, 600]
|
||||
if (width < 1080) {
|
||||
mainWindow?.setSize(1080, height)
|
||||
}
|
||||
})
|
||||
|
||||
// gemini
|
||||
ipcMain.handle('gemini:upload-file', GeminiService.uploadFile)
|
||||
ipcMain.handle('gemini:base64-file', GeminiService.base64File)
|
||||
ipcMain.handle('gemini:retrieve-file', GeminiService.retrieveFile)
|
||||
ipcMain.handle('gemini:list-files', GeminiService.listFiles)
|
||||
ipcMain.handle('gemini:delete-file', GeminiService.deleteFile)
|
||||
|
||||
// mini window
|
||||
ipcMain.handle('miniwindow:show', () => windowService.showMiniWindow())
|
||||
ipcMain.handle('miniwindow:hide', () => windowService.hideMiniWindow())
|
||||
ipcMain.handle('miniwindow:close', () => windowService.closeMiniWindow())
|
||||
ipcMain.handle('miniwindow:toggle', () => windowService.toggleMiniWindow())
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
interface CacheItem<T> {
|
||||
data: T
|
||||
timestamp: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
export class CacheService {
|
||||
private static cache: Map<string, CacheItem<any>> = new Map()
|
||||
|
||||
/**
|
||||
* Set cache
|
||||
* @param key Cache key
|
||||
* @param data Cache data
|
||||
* @param duration Cache duration (in milliseconds)
|
||||
*/
|
||||
static set<T>(key: string, data: T, duration: number): void {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
timestamp: Date.now(),
|
||||
duration
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache
|
||||
* @param key Cache key
|
||||
* @returns Returns data if cache exists and not expired, otherwise returns null
|
||||
*/
|
||||
static get<T>(key: string): T | null {
|
||||
const item = this.cache.get(key)
|
||||
if (!item) return null
|
||||
|
||||
const now = Date.now()
|
||||
if (now - item.timestamp > item.duration) {
|
||||
this.remove(key)
|
||||
return null
|
||||
}
|
||||
|
||||
return item.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove specific cache
|
||||
* @param key Cache key
|
||||
*/
|
||||
static remove(key: string): void {
|
||||
this.cache.delete(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cache
|
||||
*/
|
||||
static clear(): void {
|
||||
this.cache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if cache exists and is valid
|
||||
* @param key Cache key
|
||||
* @returns boolean
|
||||
*/
|
||||
static has(key: string): boolean {
|
||||
const item = this.cache.get(key)
|
||||
if (!item) return false
|
||||
|
||||
const now = Date.now()
|
||||
if (now - item.timestamp > item.duration) {
|
||||
this.remove(key)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
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)
|
||||
}
|
||||
|
||||
getTray(): boolean {
|
||||
isTray(): boolean {
|
||||
return !!this.store.get('tray', true)
|
||||
}
|
||||
|
||||
@@ -83,30 +83,6 @@ export class ConfigManager {
|
||||
)
|
||||
this.notifySubscribers('shortcuts', shortcuts)
|
||||
}
|
||||
|
||||
getClickTrayToShowQuickAssistant(): boolean {
|
||||
return this.store.get('clickTrayToShowQuickAssistant', false) as boolean
|
||||
}
|
||||
|
||||
setClickTrayToShowQuickAssistant(value: boolean) {
|
||||
this.store.set('clickTrayToShowQuickAssistant', value)
|
||||
}
|
||||
|
||||
getEnableQuickAssistant(): boolean {
|
||||
return this.store.get('enableQuickAssistant', false) as boolean
|
||||
}
|
||||
|
||||
setEnableQuickAssistant(value: boolean) {
|
||||
this.store.set('enableQuickAssistant', value)
|
||||
}
|
||||
|
||||
set(key: string, value: any) {
|
||||
this.store.set(key, value)
|
||||
}
|
||||
|
||||
get(key: string) {
|
||||
return this.store.get(key)
|
||||
}
|
||||
}
|
||||
|
||||
export const configManager = new ConfigManager()
|
||||
|
||||
@@ -263,13 +263,6 @@ class FileStorage {
|
||||
}
|
||||
}
|
||||
|
||||
public binaryFile = async (_: Electron.IpcMainInvokeEvent, id: string): Promise<{ data: Buffer; mime: string }> => {
|
||||
const filePath = path.join(this.storageDir, id)
|
||||
const data = await fs.promises.readFile(filePath)
|
||||
const mime = `image/${path.extname(filePath).slice(1)}`
|
||||
return { data, mime }
|
||||
}
|
||||
|
||||
public clear = async (): Promise<void> => {
|
||||
await fs.promises.rmdir(this.storageDir, { recursive: true })
|
||||
await this.initStorageDir()
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { FileMetadataResponse, FileState, GoogleAIFileManager } from '@google/generative-ai/server'
|
||||
import { FileType } from '@types'
|
||||
import fs from 'fs'
|
||||
|
||||
import { CacheService } from './CacheService'
|
||||
|
||||
export class GeminiService {
|
||||
private static readonly FILE_LIST_CACHE_KEY = 'gemini_file_list'
|
||||
private static readonly CACHE_DURATION = 3000
|
||||
|
||||
static async uploadFile(_: Electron.IpcMainInvokeEvent, file: FileType, apiKey: string) {
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
const uploadResult = await fileManager.uploadFile(file.path, {
|
||||
mimeType: 'application/pdf',
|
||||
displayName: file.origin_name
|
||||
})
|
||||
return uploadResult
|
||||
}
|
||||
|
||||
static async base64File(_: Electron.IpcMainInvokeEvent, file: FileType) {
|
||||
return {
|
||||
data: Buffer.from(fs.readFileSync(file.path)).toString('base64'),
|
||||
mimeType: 'application/pdf'
|
||||
}
|
||||
}
|
||||
|
||||
static async retrieveFile(
|
||||
_: Electron.IpcMainInvokeEvent,
|
||||
file: FileType,
|
||||
apiKey: string
|
||||
): Promise<FileMetadataResponse | undefined> {
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
|
||||
const cachedResponse = CacheService.get<any>(GeminiService.FILE_LIST_CACHE_KEY)
|
||||
if (cachedResponse) {
|
||||
return GeminiService.processResponse(cachedResponse, file)
|
||||
}
|
||||
|
||||
const response = await fileManager.listFiles()
|
||||
CacheService.set(GeminiService.FILE_LIST_CACHE_KEY, response, GeminiService.CACHE_DURATION)
|
||||
|
||||
return GeminiService.processResponse(response, file)
|
||||
}
|
||||
|
||||
private static processResponse(response: any, file: FileType) {
|
||||
if (response.files) {
|
||||
return response.files
|
||||
.filter((file) => file.state === FileState.ACTIVE)
|
||||
.find((i) => i.displayName === file.origin_name && Number(i.sizeBytes) === file.size)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
static async listFiles(_: Electron.IpcMainInvokeEvent, apiKey: string) {
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
return await fileManager.listFiles()
|
||||
}
|
||||
|
||||
static async deleteFile(_: Electron.IpcMainInvokeEvent, apiKey: string, fileId: string) {
|
||||
const fileManager = new GoogleAIFileManager(apiKey)
|
||||
await fileManager.deleteFile(fileId)
|
||||
}
|
||||
}
|
||||
@@ -45,14 +45,14 @@ class KnowledgeService {
|
||||
azureOpenAIApiDeploymentName: model,
|
||||
azureOpenAIApiInstanceName: getInstanceName(baseURL),
|
||||
dimensions,
|
||||
batchSize: 10
|
||||
batchSize: 15
|
||||
})
|
||||
: new OpenAiEmbeddings({
|
||||
model,
|
||||
apiKey,
|
||||
configuration: { baseURL },
|
||||
dimensions,
|
||||
batchSize: 10
|
||||
batchSize: 15
|
||||
})
|
||||
)
|
||||
.setVectorDatabase(new LibSqlDb({ path: path.join(this.storageDir, id) }))
|
||||
@@ -89,14 +89,12 @@ class KnowledgeService {
|
||||
if (item.type === 'url') {
|
||||
const content = item.content as string
|
||||
if (content.startsWith('http')) {
|
||||
// @ts-ignore loader type
|
||||
return await ragApplication.addLoader(new WebLoader({ urlOrContent: content }), forceReload)
|
||||
}
|
||||
}
|
||||
|
||||
if (item.type === 'sitemap') {
|
||||
const content = item.content as string
|
||||
// @ts-ignore loader type
|
||||
return await ragApplication.addLoader(new SitemapLoader({ url: content }), forceReload)
|
||||
}
|
||||
|
||||
@@ -124,7 +122,7 @@ class KnowledgeService {
|
||||
return await ragApplication.addLoader(new ExcelLoader({ filePathOrUrl: file.path }) as any, forceReload)
|
||||
}
|
||||
|
||||
if (['.md'].includes(file.ext)) {
|
||||
if (['.md', '.mdx'].includes(file.ext)) {
|
||||
return await ragApplication.addLoader(new MarkdownLoader({ filePathOrUrl: file.path }) as any, forceReload)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,8 @@ import { BrowserWindow, globalShortcut } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
import { configManager } from './ConfigManager'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
let showAppAccelerator: string | null = null
|
||||
let showMiniWindowAccelerator: string | null = null
|
||||
|
||||
function getShortcutHandler(shortcut: Shortcut) {
|
||||
switch (shortcut.key) {
|
||||
@@ -28,10 +26,6 @@ function getShortcutHandler(shortcut: Shortcut) {
|
||||
window.focus()
|
||||
}
|
||||
}
|
||||
case 'mini_window':
|
||||
return () => {
|
||||
windowService.toggleMiniWindow()
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -79,10 +73,6 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
showAppAccelerator = accelerator
|
||||
}
|
||||
|
||||
if (shortcut.key === 'mini_window') {
|
||||
showMiniWindowAccelerator = accelerator
|
||||
}
|
||||
|
||||
if (shortcut.key.includes('zoom')) {
|
||||
switch (shortcut.key) {
|
||||
case 'zoom_in':
|
||||
@@ -100,7 +90,7 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
}
|
||||
|
||||
if (shortcut.enabled) {
|
||||
globalShortcut.register(formatShortcutKey(shortcut.shortcut), () => handler(window))
|
||||
globalShortcut.register(accelerator, () => handler(window))
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error(`[ShortcutService] Failed to register shortcut ${shortcut.key}`)
|
||||
@@ -118,11 +108,6 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
const handler = getShortcutHandler({ key: 'show_app' } as Shortcut)
|
||||
handler && globalShortcut.register(showAppAccelerator, () => handler(window))
|
||||
}
|
||||
|
||||
if (showMiniWindowAccelerator) {
|
||||
const handler = getShortcutHandler({ key: 'mini_window' } as Shortcut)
|
||||
handler && globalShortcut.register(showMiniWindowAccelerator, () => handler(window))
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.error('[ShortcutService] Failed to unregister shortcuts')
|
||||
}
|
||||
@@ -139,7 +124,6 @@ export function registerShortcuts(window: BrowserWindow) {
|
||||
export function unregisterAllShortcuts() {
|
||||
try {
|
||||
showAppAccelerator = null
|
||||
showMiniWindowAccelerator = null
|
||||
globalShortcut.unregisterAll()
|
||||
} catch (error) {
|
||||
Logger.error('[ShortcutService] Failed to unregister all shortcuts')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isMac } from '@main/constant'
|
||||
import { locales } from '@main/utils/locales'
|
||||
import { app, Menu, MenuItemConstructorOptions, nativeImage, nativeTheme, Tray } from 'electron'
|
||||
import { app, Menu, nativeImage, nativeTheme, Tray } from 'electron'
|
||||
|
||||
import icon from '../../../build/tray_icon.png?asset'
|
||||
import iconDark from '../../../build/tray_icon_dark.png?asset'
|
||||
@@ -9,22 +9,14 @@ import { configManager } from './ConfigManager'
|
||||
import { windowService } from './WindowService'
|
||||
|
||||
export class TrayService {
|
||||
private static instance: TrayService
|
||||
private tray: Tray | null = null
|
||||
|
||||
constructor() {
|
||||
this.updateTray()
|
||||
this.watchTrayChanges()
|
||||
TrayService.instance = this
|
||||
}
|
||||
|
||||
public static getInstance() {
|
||||
return TrayService.instance
|
||||
}
|
||||
|
||||
private createTray() {
|
||||
this.destroyTray()
|
||||
|
||||
const iconPath = isMac ? (nativeTheme.shouldUseDarkColors ? iconLight : iconDark) : icon
|
||||
const tray = new Tray(iconPath)
|
||||
|
||||
@@ -46,25 +38,17 @@ export class TrayService {
|
||||
const locale = locales[configManager.getLanguage()]
|
||||
const { tray: trayLocale } = locale.translation
|
||||
|
||||
const enableQuickAssistant = configManager.getEnableQuickAssistant()
|
||||
|
||||
const template = [
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: trayLocale.show_window,
|
||||
click: () => windowService.showMainWindow()
|
||||
},
|
||||
enableQuickAssistant && {
|
||||
label: trayLocale.show_mini_window,
|
||||
click: () => windowService.showMiniWindow()
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: trayLocale.quit,
|
||||
click: () => this.quit()
|
||||
}
|
||||
].filter(Boolean) as MenuItemConstructorOptions[]
|
||||
|
||||
const contextMenu = Menu.buildFromTemplate(template)
|
||||
])
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
this.tray.setContextMenu(contextMenu)
|
||||
@@ -77,30 +61,18 @@ export class TrayService {
|
||||
})
|
||||
|
||||
this.tray.on('click', () => {
|
||||
if (enableQuickAssistant && configManager.getClickTrayToShowQuickAssistant()) {
|
||||
windowService.showMiniWindow()
|
||||
} else {
|
||||
windowService.showMainWindow()
|
||||
}
|
||||
windowService.showMainWindow()
|
||||
})
|
||||
}
|
||||
|
||||
private updateTray() {
|
||||
const showTray = configManager.getTray()
|
||||
if (showTray) {
|
||||
if (configManager.isTray()) {
|
||||
this.createTray()
|
||||
} else {
|
||||
this.destroyTray()
|
||||
}
|
||||
}
|
||||
|
||||
public restartTray() {
|
||||
if (configManager.getTray()) {
|
||||
this.destroyTray()
|
||||
this.createTray()
|
||||
}
|
||||
}
|
||||
|
||||
private destroyTray() {
|
||||
if (this.tray) {
|
||||
this.tray.destroy()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { isLinux, isWin } from '@main/constant'
|
||||
import { app, BrowserWindow, ipcMain, Menu, MenuItem, shell } from 'electron'
|
||||
import { app, BrowserWindow, Menu, MenuItem, shell } from 'electron'
|
||||
import Logger from 'electron-log'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import path, { join } from 'path'
|
||||
@@ -13,10 +13,6 @@ import { configManager } from './ConfigManager'
|
||||
export class WindowService {
|
||||
private static instance: WindowService | null = null
|
||||
private mainWindow: BrowserWindow | null = null
|
||||
private miniWindow: BrowserWindow | null = null
|
||||
private wasFullScreen: boolean = false
|
||||
private selectionMenuWindow: BrowserWindow | null = null
|
||||
private lastSelectedText: string = ''
|
||||
|
||||
public static getInstance(): WindowService {
|
||||
if (!WindowService.instance) {
|
||||
@@ -46,7 +42,7 @@ export class WindowService {
|
||||
height: mainWindowState.height,
|
||||
minWidth: 1080,
|
||||
minHeight: 600,
|
||||
show: false, // 初始不显示
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: isMac,
|
||||
vibrancy: 'under-window',
|
||||
@@ -65,7 +61,6 @@ export class WindowService {
|
||||
})
|
||||
|
||||
this.setupMainWindow(this.mainWindow, mainWindowState)
|
||||
|
||||
return this.mainWindow
|
||||
}
|
||||
|
||||
@@ -123,20 +118,9 @@ export class WindowService {
|
||||
}
|
||||
|
||||
private setupWindowEvents(mainWindow: BrowserWindow) {
|
||||
mainWindow.once('ready-to-show', () => {
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
// 处理全屏相关事件
|
||||
mainWindow.on('enter-full-screen', () => {
|
||||
this.wasFullScreen = true
|
||||
mainWindow.webContents.send('fullscreen-status-changed', true)
|
||||
})
|
||||
|
||||
mainWindow.on('leave-full-screen', () => {
|
||||
this.wasFullScreen = false
|
||||
mainWindow.webContents.send('fullscreen-status-changed', false)
|
||||
})
|
||||
}
|
||||
|
||||
private setupWebContentsHandlers(mainWindow: BrowserWindow) {
|
||||
@@ -199,24 +183,18 @@ export class WindowService {
|
||||
|
||||
private setupWindowLifecycleEvents(mainWindow: BrowserWindow) {
|
||||
mainWindow.on('close', (event) => {
|
||||
// 如果已经触发退出,直接退出
|
||||
if (app.isQuitting) {
|
||||
return app.quit()
|
||||
}
|
||||
const notInTray = !configManager.isTray()
|
||||
|
||||
// 没有开启托盘,且是Windows或Linux系统,直接退出
|
||||
const notInTray = !configManager.getTray()
|
||||
// Windows and Linux
|
||||
if ((isWin || isLinux) && notInTray) {
|
||||
return app.quit()
|
||||
}
|
||||
|
||||
// 如果是全屏状态,直接退出
|
||||
if (this.wasFullScreen) {
|
||||
return app.quit()
|
||||
// Mac
|
||||
if (!app.isQuitting) {
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
}
|
||||
|
||||
event.preventDefault()
|
||||
mainWindow.hide()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -231,164 +209,6 @@ export class WindowService {
|
||||
this.createMainWindow()
|
||||
}
|
||||
}
|
||||
|
||||
public showMiniWindow() {
|
||||
const enableQuickAssistant = configManager.getEnableQuickAssistant()
|
||||
|
||||
if (!enableQuickAssistant) {
|
||||
return
|
||||
}
|
||||
|
||||
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: true,
|
||||
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('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'), {
|
||||
hash: '#/mini'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public hideMiniWindow() {
|
||||
this.miniWindow?.hide()
|
||||
}
|
||||
|
||||
public closeMiniWindow() {
|
||||
this.miniWindow?.close()
|
||||
}
|
||||
|
||||
public toggleMiniWindow() {
|
||||
if (this.miniWindow) {
|
||||
this.miniWindow.isVisible() ? this.miniWindow.hide() : this.miniWindow.show()
|
||||
} else {
|
||||
this.showMiniWindow()
|
||||
}
|
||||
}
|
||||
|
||||
public showSelectionMenu(bounds: { x: number; y: number }) {
|
||||
if (this.selectionMenuWindow && !this.selectionMenuWindow.isDestroyed()) {
|
||||
this.selectionMenuWindow.setPosition(bounds.x, bounds.y)
|
||||
this.selectionMenuWindow.show()
|
||||
return
|
||||
}
|
||||
|
||||
const theme = configManager.getTheme()
|
||||
const isMac = process.platform === 'darwin'
|
||||
|
||||
this.selectionMenuWindow = new BrowserWindow({
|
||||
width: 280,
|
||||
height: 40,
|
||||
x: bounds.x,
|
||||
y: bounds.y,
|
||||
show: true,
|
||||
autoHideMenuBar: true,
|
||||
transparent: true,
|
||||
frame: false,
|
||||
alwaysOnTop: false,
|
||||
skipTaskbar: true,
|
||||
backgroundColor: isMac ? undefined : theme === 'dark' ? '#181818' : '#FFFFFF',
|
||||
resizable: false,
|
||||
vibrancy: 'popover',
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false,
|
||||
webSecurity: false
|
||||
}
|
||||
})
|
||||
|
||||
// 点击其他地方时隐藏窗口
|
||||
this.selectionMenuWindow.on('blur', () => {
|
||||
this.selectionMenuWindow?.hide()
|
||||
this.miniWindow?.webContents.send('selection-action', {
|
||||
action: 'home',
|
||||
selectedText: this.lastSelectedText
|
||||
})
|
||||
})
|
||||
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
this.selectionMenuWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/src/windows/menu/menu.html')
|
||||
} else {
|
||||
this.selectionMenuWindow.loadFile(join(__dirname, '../renderer/src/windows/menu/menu.html'))
|
||||
}
|
||||
|
||||
this.setupSelectionMenuEvents()
|
||||
}
|
||||
|
||||
private setupSelectionMenuEvents() {
|
||||
if (!this.selectionMenuWindow) return
|
||||
|
||||
ipcMain.removeHandler('selection-menu:action')
|
||||
ipcMain.handle('selection-menu:action', (_, action) => {
|
||||
this.selectionMenuWindow?.hide()
|
||||
this.showMiniWindow()
|
||||
setTimeout(() => {
|
||||
this.miniWindow?.webContents.send('selection-action', {
|
||||
action,
|
||||
selectedText: this.lastSelectedText
|
||||
})
|
||||
}, 100)
|
||||
})
|
||||
}
|
||||
|
||||
public setLastSelectedText(text: string) {
|
||||
this.lastSelectedText = text
|
||||
}
|
||||
}
|
||||
|
||||
export const windowService = WindowService.getInstance()
|
||||
|
||||
@@ -22,23 +22,3 @@ export function getInstanceName(baseURL: string) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export function debounce(func: (...args: any[]) => void, wait: number, immediate: boolean = false) {
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
return function (...args: any[]) {
|
||||
if (timeout) clearTimeout(timeout)
|
||||
if (immediate) {
|
||||
func(...args)
|
||||
} else {
|
||||
timeout = setTimeout(() => func(...args), wait)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function dumpPersistState() {
|
||||
const persistState = JSON.parse(localStorage.getItem('persist:cherry-studio') || '{}')
|
||||
for (const key in persistState) {
|
||||
persistState[key] = JSON.parse(persistState[key])
|
||||
}
|
||||
return JSON.stringify(persistState)
|
||||
}
|
||||
|
||||
28
src/preload/index.d.ts
vendored
@@ -1,5 +1,4 @@
|
||||
import { ElectronAPI } from '@electron-toolkit/preload'
|
||||
import type { FileMetadataResponse, ListFilesResponse, UploadFileResponse } from '@google/generative-ai/server'
|
||||
import { AddLoaderReturn, ExtractChunkData } from '@llm-tools/embedjs-interfaces'
|
||||
import { FileType } from '@renderer/types'
|
||||
import { WebDavConfig } from '@renderer/types'
|
||||
@@ -18,7 +17,6 @@ declare global {
|
||||
setProxy: (proxy: string | undefined) => void
|
||||
setLanguage: (theme: LanguageVarious) => void
|
||||
setTray: (isActive: boolean) => void
|
||||
restartTray: () => void
|
||||
setTheme: (theme: 'light' | 'dark') => void
|
||||
minApp: (options: { url: string; windowOptions?: Electron.BrowserWindowConstructorOptions }) => void
|
||||
reload: () => void
|
||||
@@ -54,7 +52,6 @@ declare global {
|
||||
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
|
||||
download: (url: string) => Promise<FileType | null>
|
||||
copy: (fileId: string, destPath: string) => Promise<void>
|
||||
binaryFile: (fileId: string) => Promise<{ data: Buffer; mime: string }>
|
||||
}
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => Promise<void>
|
||||
@@ -79,29 +76,8 @@ declare global {
|
||||
remove: ({ uniqueId, base }: { uniqueId: string; base: KnowledgeBaseParams }) => Promise<void>
|
||||
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) => Promise<ExtractChunkData[]>
|
||||
}
|
||||
window: {
|
||||
setMinimumSize: (width: number, height: number) => Promise<void>
|
||||
resetMinimumSize: () => Promise<void>
|
||||
}
|
||||
gemini: {
|
||||
uploadFile: (file: FileType, apiKey: string) => Promise<UploadFileResponse>
|
||||
retrieveFile: (file: FileType, apiKey: string) => Promise<FileMetadataResponse | undefined>
|
||||
base64File: (file: FileType) => Promise<{ data: string; mimeType: string }>
|
||||
listFiles: (apiKey: string) => Promise<ListFilesResponse>
|
||||
deleteFile: (apiKey: string, fileId: string) => Promise<void>
|
||||
}
|
||||
selectionMenu: {
|
||||
action: (action: string) => Promise<void>
|
||||
}
|
||||
config: {
|
||||
set: (key: string, value: any) => Promise<void>
|
||||
get: (key: string) => Promise<any>
|
||||
}
|
||||
miniWindow: {
|
||||
show: () => Promise<void>
|
||||
hide: () => Promise<void>
|
||||
close: () => Promise<void>
|
||||
toggle: () => Promise<void>
|
||||
vm: {
|
||||
run: (code: string) => Promise<any>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
import { FileType, KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
|
||||
import { KnowledgeBaseParams, KnowledgeItem, Shortcut, WebDavConfig } from '@types'
|
||||
import { contextBridge, ipcRenderer, OpenDialogOptions } from 'electron'
|
||||
|
||||
// Custom APIs for renderer
|
||||
@@ -10,7 +10,6 @@ const api = {
|
||||
checkForUpdate: () => ipcRenderer.invoke('app:check-for-update'),
|
||||
setLanguage: (lang: string) => ipcRenderer.invoke('app:set-language', lang),
|
||||
setTray: (isActive: boolean) => ipcRenderer.invoke('app:set-tray', isActive),
|
||||
restartTray: () => ipcRenderer.invoke('app:restart-tray'),
|
||||
setTheme: (theme: 'light' | 'dark') => ipcRenderer.invoke('app:set-theme', theme),
|
||||
openWebsite: (url: string) => ipcRenderer.invoke('open:website', url),
|
||||
minApp: (url: string) => ipcRenderer.invoke('minapp', url),
|
||||
@@ -44,8 +43,7 @@ const api = {
|
||||
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data),
|
||||
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId),
|
||||
download: (url: string) => ipcRenderer.invoke('file:download', url),
|
||||
copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath),
|
||||
binaryFile: (fileId: string) => ipcRenderer.invoke('file:binaryFile', fileId)
|
||||
copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath)
|
||||
},
|
||||
export: {
|
||||
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName)
|
||||
@@ -73,29 +71,8 @@ const api = {
|
||||
search: ({ search, base }: { search: string; base: KnowledgeBaseParams }) =>
|
||||
ipcRenderer.invoke('knowledge-base:search', { search, base })
|
||||
},
|
||||
window: {
|
||||
setMinimumSize: (width: number, height: number) => ipcRenderer.invoke('window:set-minimum-size', width, height),
|
||||
resetMinimumSize: () => ipcRenderer.invoke('window:reset-minimum-size')
|
||||
},
|
||||
gemini: {
|
||||
uploadFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:upload-file', file, apiKey),
|
||||
base64File: (file: FileType) => ipcRenderer.invoke('gemini:base64-file', file),
|
||||
retrieveFile: (file: FileType, apiKey: string) => ipcRenderer.invoke('gemini:retrieve-file', file, apiKey),
|
||||
listFiles: (apiKey: string) => ipcRenderer.invoke('gemini:list-files', apiKey),
|
||||
deleteFile: (apiKey: string, fileId: string) => ipcRenderer.invoke('gemini:delete-file', apiKey, fileId)
|
||||
},
|
||||
selectionMenu: {
|
||||
action: (action: string) => ipcRenderer.invoke('selection-menu:action', action)
|
||||
},
|
||||
config: {
|
||||
set: (key: string, value: any) => ipcRenderer.invoke('config:set', key, value),
|
||||
get: (key: string) => ipcRenderer.invoke('config:get', key)
|
||||
},
|
||||
miniWindow: {
|
||||
show: () => ipcRenderer.invoke('miniwindow:show'),
|
||||
hide: () => ipcRenderer.invoke('miniwindow:hide'),
|
||||
close: () => ipcRenderer.invoke('miniwindow:close'),
|
||||
toggle: () => ipcRenderer.invoke('miniwindow:toggle')
|
||||
vm: {
|
||||
run: (code: string) => ipcRenderer.invoke('run-js', code)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
position: fixed;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#spinner img {
|
||||
@@ -35,7 +35,6 @@
|
||||
<div id="spinner">
|
||||
<img src="/src/assets/images/logo.png" />
|
||||
</div>
|
||||
<script type="module" src="/src/init.ts"></script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -1,91 +1,88 @@
|
||||
@font-face {
|
||||
font-family: "iconfont"; /* Project id 4753420 */
|
||||
src: url('iconfont.woff2?t=1736309723926') format('woff2'),
|
||||
url('iconfont.woff?t=1736309723926') format('woff'),
|
||||
url('iconfont.ttf?t=1736309723926') format('truetype');
|
||||
font-family: 'iconfont'; /* Project id 4753420 */
|
||||
src: url('iconfont.woff2?t=1733224456443') format('woff2');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-family: "iconfont" !important;
|
||||
font-family: 'iconfont' !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-at:before {
|
||||
content: "\e623";
|
||||
.icon-at1:before {
|
||||
content: '\e7df';
|
||||
}
|
||||
|
||||
.icon-icon-adaptive-width:before {
|
||||
content: "\e87a";
|
||||
.icon-at:before {
|
||||
content: '\e630';
|
||||
}
|
||||
|
||||
.icon-a-darkmode:before {
|
||||
content: "\e6cd";
|
||||
content: '\e6cd';
|
||||
}
|
||||
|
||||
.icon-ai-model:before {
|
||||
content: "\e827";
|
||||
content: '\e827';
|
||||
}
|
||||
|
||||
.icon-ai-model1:before {
|
||||
content: "\ec09";
|
||||
content: '\ec09';
|
||||
}
|
||||
|
||||
.icon-gridlines:before {
|
||||
content: "\e942";
|
||||
content: '\e942';
|
||||
}
|
||||
|
||||
.icon-inbox:before {
|
||||
content: "\e869";
|
||||
content: '\e869';
|
||||
}
|
||||
|
||||
.icon-business-smart-assistant:before {
|
||||
content: "\e601";
|
||||
content: '\e601';
|
||||
}
|
||||
|
||||
.icon-copy:before {
|
||||
content: "\e6ae";
|
||||
content: '\e6ae';
|
||||
}
|
||||
|
||||
.icon-ic_send:before {
|
||||
content: "\e795";
|
||||
content: '\e795';
|
||||
}
|
||||
|
||||
.icon-dark1:before {
|
||||
content: "\e72f";
|
||||
content: '\e72f';
|
||||
}
|
||||
|
||||
.icon-theme-light:before {
|
||||
content: "\e6b7";
|
||||
content: '\e6b7';
|
||||
}
|
||||
|
||||
.icon-translate_line:before {
|
||||
content: "\e7de";
|
||||
content: '\e7de';
|
||||
}
|
||||
|
||||
.icon-history:before {
|
||||
content: "\e758";
|
||||
content: '\e758';
|
||||
}
|
||||
|
||||
.icon-hide-sidebar:before {
|
||||
content: "\e8eb";
|
||||
content: '\e8eb';
|
||||
}
|
||||
|
||||
.icon-show-sidebar:before {
|
||||
content: "\e944";
|
||||
content: '\e944';
|
||||
}
|
||||
|
||||
.icon-appstore:before {
|
||||
content: "\e792";
|
||||
content: '\e792';
|
||||
}
|
||||
|
||||
.icon-chat:before {
|
||||
content: "\e615";
|
||||
content: '\e615';
|
||||
}
|
||||
|
||||
.icon-setting:before {
|
||||
content: "\e78e";
|
||||
content: '\e78e';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
<svg width="464" height="464" viewBox="0 0 464 464" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="464" height="464" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M243 127C235.268 127 229 133.268 229 141V322C229 329.732 235.268 336 243 336H283C290.732 336 297 329.732 297 322V141C297 133.268 290.732 127 283 127H243ZM167.562 128C163.762 128 160.317 129.518 157.805 131.978C157.787 131.995 157.759 131.977 157.767 131.954C157.775 131.93 157.743 131.913 157.727 131.933L157.311 132.486C156.679 133.171 156.115 133.92 155.629 134.722C154.303 136.486 153.139 138.365 152.152 140.338L88.8745 266.857L85.2894 274.899C85.2249 275.037 85.1626 275.177 85.1027 275.318L84.7141 276.189C84.7086 276.201 84.7223 276.213 84.7339 276.206C84.745 276.2 84.7583 276.211 84.7541 276.223C84.2654 277.639 84 279.16 84 280.742L84 322.399C84 330.067 90.2354 336.284 97.9271 336.284H139.708C147.4 336.284 153.635 330.067 153.635 322.399V266.857L153.636 252.97C153.636 222.295 178.577 197.428 209.344 197.428C217.035 197.428 223.271 191.211 223.271 183.542V141.886C223.271 134.217 217.035 128 209.344 128H167.562ZM304.5 301.57C304.5 282.398 320.088 266.856 339.318 266.856C358.547 266.856 374.135 282.398 374.135 301.57C374.135 320.742 358.547 336.284 339.318 336.284C320.088 336.284 304.5 320.742 304.5 301.57Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 81 KiB |
@@ -24,7 +24,6 @@
|
||||
--color-background: var(--color-black);
|
||||
--color-background-soft: var(--color-black-soft);
|
||||
--color-background-mute: var(--color-black-mute);
|
||||
--color-background-opacity: rgba(34, 34, 34, 0.7);
|
||||
|
||||
--color-primary: #00b96b;
|
||||
--color-primary-soft: #00b96b99;
|
||||
@@ -43,9 +42,6 @@
|
||||
--color-active: rgba(55, 55, 55, 1);
|
||||
--color-frame-border: #333;
|
||||
--color-group-background: var(--color-background-soft);
|
||||
|
||||
--color-reference: #404040;
|
||||
--color-reference-text: #ffffff;
|
||||
--color-reference-background: #0b0e12;
|
||||
|
||||
--navbar-background-mac: rgba(30, 30, 30, 0.6);
|
||||
@@ -64,8 +60,6 @@
|
||||
--chat-background-user: #28b561;
|
||||
--chat-background-assistant: #2c2c2c;
|
||||
--chat-text-user: var(--color-black);
|
||||
|
||||
--list-item-border-radius: 16px;
|
||||
}
|
||||
|
||||
body[theme-mode='light'] {
|
||||
@@ -88,7 +82,6 @@ body[theme-mode='light'] {
|
||||
--color-background: var(--color-white);
|
||||
--color-background-soft: var(--color-white-soft);
|
||||
--color-background-mute: var(--color-white-mute);
|
||||
--color-background-opacity: rgba(255, 255, 255, 0.7);
|
||||
|
||||
--color-primary: #00b96b;
|
||||
--color-primary-soft: #00b96b99;
|
||||
@@ -107,9 +100,6 @@ body[theme-mode='light'] {
|
||||
--color-active: var(--color-white-soft);
|
||||
--color-frame-border: #ddd;
|
||||
--color-group-background: var(--color-white);
|
||||
|
||||
--color-reference: #cfe1ff;
|
||||
--color-reference-text: #000000;
|
||||
--color-reference-background: #f1f7ff;
|
||||
|
||||
--navbar-background-mac: rgba(255, 255, 255, 0.6);
|
||||
@@ -179,9 +169,12 @@ body,
|
||||
#content-container {
|
||||
background-color: var(--color-background);
|
||||
border-top: 0.5px solid var(--color-border);
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
#content-container {
|
||||
border-top-left-radius: 12px;
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.06);
|
||||
box-shadow: -2px 0px 20px -4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.loader {
|
||||
@@ -223,7 +216,10 @@ body,
|
||||
background-color: var(--chat-background);
|
||||
}
|
||||
#inputbar {
|
||||
margin: -5px 15px 15px 15px;
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border-mute);
|
||||
background: var(--color-background);
|
||||
}
|
||||
.system-prompt {
|
||||
|
||||
@@ -208,14 +208,6 @@
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-reference);
|
||||
color: var(--color-reference-text);
|
||||
padding: 2px 5px;
|
||||
zoom: 0.8;
|
||||
& > span.link {
|
||||
color: var(--color-reference-text);
|
||||
}
|
||||
}
|
||||
|
||||
sub {
|
||||
@@ -234,55 +226,51 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footnotes {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
padding-top: 1em;
|
||||
.footnotes {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
padding-top: 1em;
|
||||
|
||||
background-color: var(--color-reference-background);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--color-reference-background);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
|
||||
h4 {
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--color-link);
|
||||
}
|
||||
|
||||
ol {
|
||||
padding-left: 1em;
|
||||
margin: 0;
|
||||
li:last-child {
|
||||
margin-bottom: 0;
|
||||
h4 {
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--color-text-light);
|
||||
|
||||
p {
|
||||
display: inline;
|
||||
ol {
|
||||
padding-left: 1em;
|
||||
margin: 0;
|
||||
li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footnote-backref {
|
||||
font-size: 0.8em;
|
||||
vertical-align: super;
|
||||
line-height: 0;
|
||||
margin-left: 5px;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
li {
|
||||
font-size: 0.9em;
|
||||
margin-bottom: 0.5em;
|
||||
color: var(--color-text-light);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footnote-backref {
|
||||
font-size: 0.8em;
|
||||
vertical-align: super;
|
||||
line-height: 0;
|
||||
margin-left: 5px;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,22 +47,19 @@ const DragableList: FC<Props<any>> = ({
|
||||
<Droppable droppableId="droppable" {...droppableProps}>
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} style={{ ...style }}>
|
||||
{list.map((item, index) => {
|
||||
const id = item.id || item
|
||||
return (
|
||||
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index} {...droppableProps}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
|
||||
{children(item, index)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
})}
|
||||
{list.map((item, index) => (
|
||||
<Draggable key={`draggable_${item.id}_${index}`} draggableId={item.id} index={index} {...droppableProps}>
|
||||
{(provided) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{ ...provided.draggableProps.style, marginBottom: 8, ...listStyle }}>
|
||||
{children(item, index)}
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
app: MinAppType
|
||||
size?: number
|
||||
style?: React.CSSProperties
|
||||
}
|
||||
|
||||
const MinAppIcon: FC<Props> = ({ app, size = 48, style }) => {
|
||||
const _app = DEFAULT_MIN_APPS.find((item) => item.id === app.id)
|
||||
|
||||
if (!_app) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Container
|
||||
src={_app.logo}
|
||||
style={{
|
||||
border: _app.bodered ? '0.5px solid var(--color-border)' : 'none',
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
backgroundColor: _app.background,
|
||||
...style
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.img`
|
||||
border-radius: 16px;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
`
|
||||
|
||||
export default MinAppIcon
|
||||
@@ -10,8 +10,9 @@ interface ListItemProps {
|
||||
}
|
||||
|
||||
const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) => {
|
||||
const borderRadius = subtitle ? '10px' : '16px'
|
||||
return (
|
||||
<ListItemContainer className={active ? 'active' : ''} onClick={onClick}>
|
||||
<ListItemContainer className={active ? 'active' : ''} onClick={onClick} style={{ borderRadius }}>
|
||||
<ListItemContent>
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
<TextContainer>
|
||||
@@ -25,7 +26,7 @@ const ListItem = ({ active, icon, title, subtitle, onClick }: ListItemProps) =>
|
||||
|
||||
const ListItemContainer = styled.div`
|
||||
padding: 7px 12px;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 16px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
/* eslint-disable react/no-unknown-property */
|
||||
import { CloseOutlined, ExportOutlined, PushpinOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { CloseOutlined, ExportOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { useBridge } from '@renderer/hooks/useBridge'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import store from '@renderer/store'
|
||||
import { setMinappShow } from '@renderer/store/runtime'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import { delay } from '@renderer/utils'
|
||||
import { Avatar, Drawer } from 'antd'
|
||||
import { WebviewTag } from 'electron'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
@@ -22,8 +19,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||
const isPinned = pinned.some((p) => p.id === app.id)
|
||||
const [open, setOpen] = useState(true)
|
||||
const [opened, setOpened] = useState(false)
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
@@ -32,12 +27,10 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
useBridge()
|
||||
|
||||
const canOpenExternalLink = app.url.startsWith('http://') || app.url.startsWith('https://')
|
||||
const canPinned = DEFAULT_MIN_APPS.some((i) => i.id === app?.id)
|
||||
|
||||
const onClose = async (_delay = 0.3) => {
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
await delay(_delay)
|
||||
resolve({})
|
||||
setTimeout(() => resolve({}), 300)
|
||||
}
|
||||
|
||||
MinApp.onClose = onClose
|
||||
@@ -52,11 +45,6 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
window.api.openWebsite(app.url)
|
||||
}
|
||||
|
||||
const onTogglePin = () => {
|
||||
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...pinned, app]
|
||||
updatePinnedMinapps(newPinned)
|
||||
}
|
||||
|
||||
const Title = () => {
|
||||
return (
|
||||
<TitleContainer style={{ justifyContent: 'space-between' }}>
|
||||
@@ -65,17 +53,12 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
<Button onClick={onReload}>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
{canPinned && (
|
||||
<Button onClick={onTogglePin} className={isPinned ? 'pinned' : ''}>
|
||||
<PushpinOutlined style={{ fontSize: 16 }} />
|
||||
</Button>
|
||||
)}
|
||||
{canOpenExternalLink && (
|
||||
<Button onClick={onOpenLink}>
|
||||
<ExportOutlined />
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => onClose()}>
|
||||
<Button onClick={onClose}>
|
||||
<CloseOutlined />
|
||||
</Button>
|
||||
</ButtonsGroup>
|
||||
@@ -116,7 +99,7 @@ const PopupContainer: React.FC<Props> = ({ app, resolve }) => {
|
||||
<Drawer
|
||||
title={<Title />}
|
||||
placement="bottom"
|
||||
onClose={() => onClose()}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
mask={true}
|
||||
rootClassName="minapp-drawer"
|
||||
@@ -155,7 +138,7 @@ const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: ${isMac ? '20px' : '10px'};
|
||||
padding-left: ${isMac ? '20px' : '15px'};
|
||||
padding-right: 10px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -203,10 +186,6 @@ const Button = styled.div`
|
||||
color: var(--color-text-1);
|
||||
background-color: var(--color-background-mute);
|
||||
}
|
||||
&.pinned {
|
||||
color: var(--color-primary);
|
||||
background-color: var(--color-primary-bg);
|
||||
}
|
||||
`
|
||||
|
||||
const EmptyView = styled.div`
|
||||
@@ -223,22 +202,12 @@ const EmptyView = styled.div`
|
||||
export default class MinApp {
|
||||
static topviewId = 0
|
||||
static onClose = () => {}
|
||||
static app: MinAppType | null = null
|
||||
|
||||
static async start(app: MinAppType) {
|
||||
if (app?.id && MinApp.app?.id === app?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
if (MinApp.app) {
|
||||
// @ts-ignore delay params
|
||||
await MinApp.onClose(0)
|
||||
await delay(0)
|
||||
}
|
||||
|
||||
MinApp.app = app
|
||||
static close() {
|
||||
TopView.hide('MinApp')
|
||||
store.dispatch(setMinappShow(false))
|
||||
}
|
||||
static start(app: MinAppType) {
|
||||
store.dispatch(setMinappShow(true))
|
||||
|
||||
return new Promise<any>((resolve) => {
|
||||
TopView.show(
|
||||
<PopupContainer
|
||||
@@ -252,10 +221,4 @@ export default class MinApp {
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
static close() {
|
||||
TopView.hide('MinApp')
|
||||
store.dispatch(setMinappShow(false))
|
||||
MinApp.app = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import { Model } from '@renderer/types'
|
||||
import { isFreeModel } from '@renderer/utils'
|
||||
import { Tag } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import VisionIcon from './Icons/VisionIcon'
|
||||
import WebSearchIcon from './Icons/WebSearchIcon'
|
||||
|
||||
interface ModelTagsProps {
|
||||
model: Model
|
||||
showFree?: boolean
|
||||
}
|
||||
|
||||
const ModelTags: FC<ModelTagsProps> = ({ model, showFree = true }) => {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
{isVisionModel(model) && <VisionIcon />}
|
||||
{isWebSearchModel(model) && <WebSearchIcon />}
|
||||
{showFree && isFreeModel(model) && (
|
||||
<Tag style={{ marginLeft: 10 }} color="green">
|
||||
{t('models.free')}
|
||||
</Tag>
|
||||
)}
|
||||
{isEmbeddingModel(model) && (
|
||||
<Tag style={{ marginLeft: 10 }} color="orange">
|
||||
{t('models.embedding')}
|
||||
</Tag>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModelTags
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Center } from '@renderer/components/Layout'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { getAllMinApps } from '@renderer/config/minapps'
|
||||
import App from '@renderer/pages/apps/App'
|
||||
import { Popover } from 'antd'
|
||||
import { Empty } from 'antd'
|
||||
@@ -14,9 +14,9 @@ interface Props {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const MinAppsPopover: FC<Props> = ({ children }) => {
|
||||
const AppStorePopover: FC<Props> = ({ children }) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { minapps } = useMinapps()
|
||||
const apps = getAllMinApps()
|
||||
|
||||
useHotkeys('esc', () => {
|
||||
setOpen(false)
|
||||
@@ -29,10 +29,10 @@ const MinAppsPopover: FC<Props> = ({ children }) => {
|
||||
const content = (
|
||||
<PopoverContent>
|
||||
<AppsContainer>
|
||||
{minapps.map((app) => (
|
||||
{apps.map((app) => (
|
||||
<App key={app.id} app={app} onClick={handleClose} size={50} />
|
||||
))}
|
||||
{isEmpty(minapps) && (
|
||||
{isEmpty(apps) && (
|
||||
<Center>
|
||||
<Empty />
|
||||
</Center>
|
||||
@@ -48,7 +48,7 @@ const MinAppsPopover: FC<Props> = ({ children }) => {
|
||||
content={content}
|
||||
trigger="click"
|
||||
placement="bottomRight"
|
||||
styles={{ body: { padding: 25 } }}>
|
||||
overlayInnerStyle={{ padding: 25 }}>
|
||||
{children}
|
||||
</Popover>
|
||||
)
|
||||
@@ -59,7 +59,7 @@ const PopoverContent = styled(Scrollbar)``
|
||||
const AppsContainer = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(90px, 1fr));
|
||||
gap: 18px;
|
||||
gap: 25px;
|
||||
`
|
||||
|
||||
export default MinAppsPopover
|
||||
export default AppStorePopover
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PushpinOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import VisionIcon from '@renderer/components/Icons/VisionIcon'
|
||||
import { TopView } from '@renderer/components/TopView'
|
||||
import { getModelLogo, isEmbeddingModel } from '@renderer/config/models'
|
||||
import { getModelLogo, isEmbeddingModel, isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
@@ -11,8 +12,8 @@ import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import WebSearchIcon from '../Icons/WebSearchIcon'
|
||||
import { HStack } from '../Layout'
|
||||
import ModelTags from '../ModelTags'
|
||||
import Scrollbar from '../Scrollbar'
|
||||
|
||||
type MenuItem = Required<MenuProps>['items'][number]
|
||||
@@ -32,7 +33,6 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
const { providers } = useProviders()
|
||||
const [pinnedModels, setPinnedModels] = useState<string[]>([])
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const loadPinnedModels = async () => {
|
||||
@@ -75,7 +75,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
label: (
|
||||
<ModelItem>
|
||||
<span>
|
||||
{m?.name} <ModelTags model={m} />
|
||||
{m?.name} {isVisionModel(m) && <VisionIcon />} {isWebSearchModel(m) && <WebSearchIcon />}
|
||||
</span>
|
||||
<PinIcon
|
||||
onClick={(e) => {
|
||||
@@ -118,7 +118,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
key: getModelUniqId(m) + '_pinned',
|
||||
label: (
|
||||
<ModelItem>
|
||||
{m?.name} <ModelTags model={m} />
|
||||
{m?.name} {isVisionModel(m) && <VisionIcon />}
|
||||
<PinIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
@@ -163,17 +163,6 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
open && setTimeout(() => inputRef.current?.focus(), 0)
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
if (open && model) {
|
||||
setTimeout(() => {
|
||||
const selectedElement = document.querySelector('.ant-menu-item-selected')
|
||||
if (selectedElement && scrollContainerRef.current) {
|
||||
selectedElement.scrollIntoView({ block: 'center', behavior: 'auto' })
|
||||
}
|
||||
}, 100) // Small delay to ensure menu is rendered
|
||||
}
|
||||
}, [open, model])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
centered
|
||||
@@ -211,7 +200,7 @@ const PopupContainer: React.FC<PopupContainerProps> = ({ model, resolve }) => {
|
||||
/>
|
||||
</HStack>
|
||||
<Divider style={{ margin: 0, borderBlockStartWidth: 0.5 }} />
|
||||
<Scrollbar style={{ height: '50vh' }} ref={scrollContainerRef}>
|
||||
<Scrollbar style={{ height: '50vh' }}>
|
||||
<Container>
|
||||
{filteredItems.length > 0 ? (
|
||||
<StyledMenu
|
||||
|
||||
@@ -70,7 +70,6 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
|
||||
ref={textareaRef}
|
||||
rows={2}
|
||||
autoFocus
|
||||
spellCheck={false}
|
||||
{...textareaProps}
|
||||
value={textValue}
|
||||
onInput={resizeTextArea}
|
||||
|
||||
@@ -55,7 +55,7 @@ const NavbarCenterContainer = styled.div`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 ${isMac ? '20px' : 0};
|
||||
padding: 0 ${isMac ? '20px' : '15px'};
|
||||
font-weight: bold;
|
||||
color: var(--color-text-1);
|
||||
`
|
||||
|
||||
@@ -3,20 +3,15 @@ import { isMac } from '@renderer/config/constant'
|
||||
import { isLocalAi, UserAvatar } from '@renderer/config/env'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Tooltip } from 'antd'
|
||||
import { Avatar } from 'antd'
|
||||
import { Dropdown } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import DragableList from '../DragableList'
|
||||
import MinAppIcon from '../Icons/MinAppIcon'
|
||||
import MinApp from '../MinApp'
|
||||
import UserPopup from '../Popups/UserPopup'
|
||||
|
||||
@@ -24,21 +19,25 @@ const Sidebar: FC = () => {
|
||||
const { pathname } = useLocation()
|
||||
const avatar = useAvatar()
|
||||
const { minappShow } = useRuntime()
|
||||
const { generating } = useRuntime()
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const { windowStyle, sidebarIcons } = useSettings()
|
||||
const { windowStyle, showMinappIcon, showFilesIcon } = useSettings()
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
const { pinned } = useMinapps()
|
||||
|
||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
|
||||
|
||||
const onEditUser = () => UserPopup.show()
|
||||
|
||||
const macTransparentWindow = isMac && windowStyle === 'transparent'
|
||||
const sidebarBgColor = macTransparentWindow ? 'transparent' : 'var(--navbar-background)'
|
||||
|
||||
const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
|
||||
|
||||
const to = async (path: string) => {
|
||||
await modelGenerating()
|
||||
const to = (path: string) => {
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'switch-assistant' })
|
||||
return
|
||||
}
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
@@ -50,19 +49,63 @@ const Sidebar: FC = () => {
|
||||
zIndex: minappShow ? 10000 : 'initial'
|
||||
}}>
|
||||
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
||||
<MainMenusContainer>
|
||||
<MainMenus>
|
||||
<Menus onClick={MinApp.onClose}>
|
||||
<MainMenus />
|
||||
<Tooltip title={t('assistants.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/')}>
|
||||
<Icon className={isRoute('/')}>
|
||||
<i className="iconfont icon-chat" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('agents.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/agents')}>
|
||||
<Icon className={isRoutes('/agents')}>
|
||||
<i className="iconfont icon-business-smart-assistant" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('paintings.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/paintings')}>
|
||||
<Icon className={isRoute('/paintings')}>
|
||||
<PictureOutlined style={{ fontSize: 16 }} />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('translate.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/translate')}>
|
||||
<Icon className={isRoute('/translate')}>
|
||||
<TranslationOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
{showMinappIcon && (
|
||||
<Tooltip title={t('minapp.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/apps')}>
|
||||
<Icon className={isRoute('/apps')}>
|
||||
<i className="iconfont icon-appstore" />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip title={t('knowledge_base.title')} mouseEnterDelay={0.5} placement="right">
|
||||
<StyledLink onClick={() => to('/knowledge')}>
|
||||
<Icon className={isRoute('/knowledge')}>
|
||||
<FileSearchOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
{showFilesIcon && (
|
||||
<Tooltip title={t('files.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => to('/files')}>
|
||||
<Icon className={isRoute('/files')}>
|
||||
<FolderOutlined />
|
||||
</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Menus>
|
||||
{showPinnedApps && (
|
||||
<AppsContainer>
|
||||
<Divider />
|
||||
<Menus>
|
||||
<PinnedApps />
|
||||
</Menus>
|
||||
</AppsContainer>
|
||||
)}
|
||||
</MainMenusContainer>
|
||||
</MainMenus>
|
||||
<Menus onClick={MinApp.onClose}>
|
||||
<Tooltip title={t('settings.theme.title')} mouseEnterDelay={0.8} placement="right">
|
||||
<Icon onClick={() => toggleTheme()}>
|
||||
@@ -85,88 +128,11 @@ const Sidebar: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const MainMenus: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { pathname } = useLocation()
|
||||
const { sidebarIcons } = useSettings()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const isRoute = (path: string): string => (pathname === path ? 'active' : '')
|
||||
const isRoutes = (path: string): string => (pathname.startsWith(path) ? 'active' : '')
|
||||
|
||||
const iconMap = {
|
||||
assistants: <i className="iconfont icon-chat" />,
|
||||
agents: <i className="iconfont icon-business-smart-assistant" />,
|
||||
paintings: <PictureOutlined style={{ fontSize: 16 }} />,
|
||||
translate: <TranslationOutlined />,
|
||||
minapp: <i className="iconfont icon-appstore" />,
|
||||
knowledge: <FileSearchOutlined />,
|
||||
files: <FolderOutlined />
|
||||
}
|
||||
|
||||
const pathMap = {
|
||||
assistants: '/',
|
||||
agents: '/agents',
|
||||
paintings: '/paintings',
|
||||
translate: '/translate',
|
||||
minapp: '/apps',
|
||||
knowledge: '/knowledge',
|
||||
files: '/files'
|
||||
}
|
||||
|
||||
return sidebarIcons.visible.map((icon) => {
|
||||
const path = pathMap[icon]
|
||||
const isActive = path === '/' ? isRoute(path) : isRoutes(path)
|
||||
|
||||
return (
|
||||
<Tooltip key={icon} title={t(`${icon}.title`)} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink onClick={() => navigate(path)}>
|
||||
<Icon className={isActive}>{iconMap[icon]}</Icon>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const PinnedApps: FC = () => {
|
||||
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<DragableList list={pinned} onUpdate={updatePinnedMinapps} listStyle={{ marginBottom: 5 }}>
|
||||
{(app) => {
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'togglePin',
|
||||
label: t('minapp.sidebar.remove.title'),
|
||||
onClick: () => {
|
||||
const newPinned = pinned.filter((item) => item.id !== app.id)
|
||||
updatePinnedMinapps(newPinned)
|
||||
}
|
||||
}
|
||||
]
|
||||
return (
|
||||
<Tooltip key={app.id} title={app.name} mouseEnterDelay={0.8} placement="right">
|
||||
<StyledLink>
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
||||
<Icon onClick={() => MinApp.start(app)}>
|
||||
<MinAppIcon size={20} app={app} style={{ borderRadius: 6 }} />
|
||||
</Icon>
|
||||
</Dropdown>
|
||||
</StyledLink>
|
||||
</Tooltip>
|
||||
)
|
||||
}}
|
||||
</DragableList>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
padding-bottom: 12px;
|
||||
width: var(--sidebar-width);
|
||||
min-width: var(--sidebar-width);
|
||||
height: ${isMac ? 'calc(100vh - var(--navbar-height))' : '100vh'};
|
||||
@@ -183,18 +149,15 @@ const AvatarImg = styled(Avatar)`
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
`
|
||||
const MainMenusContainer = styled.div`
|
||||
const MainMenus = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const Menus = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
`
|
||||
|
||||
const Icon = styled.div`
|
||||
@@ -204,6 +167,7 @@ const Icon = styled.div`
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 5px;
|
||||
-webkit-app-region: none;
|
||||
border: 0.5px solid transparent;
|
||||
.iconfont,
|
||||
@@ -241,24 +205,4 @@ const StyledLink = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const AppsContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
margin-bottom: 10px;
|
||||
-webkit-app-region: none;
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const Divider = styled.div`
|
||||
width: 50%;
|
||||
margin: 8px 0;
|
||||
border-bottom: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
export default Sidebar
|
||||
|
||||
@@ -1,43 +1,39 @@
|
||||
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png?url'
|
||||
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp?url'
|
||||
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg?url'
|
||||
import DevvAppLogo from '@renderer/assets/images/apps/devv.png?url'
|
||||
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png?url'
|
||||
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp?url'
|
||||
import FeloAppLogo from '@renderer/assets/images/apps/felo.png?url'
|
||||
import FlowithAppLogo from '@renderer/assets/images/apps/flowith.svg?url'
|
||||
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png?url'
|
||||
import GensparkLogo from '@renderer/assets/images/apps/genspark.jpg?url'
|
||||
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp?url'
|
||||
import GrokAppLogo from '@renderer/assets/images/apps/grok.png?url'
|
||||
import HikaLogo from '@renderer/assets/images/apps/hika.webp?url'
|
||||
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg?url'
|
||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg?url'
|
||||
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp?url'
|
||||
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm.webp?url'
|
||||
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp?url'
|
||||
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp?url'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png?url'
|
||||
import QwenlmAppLogo from '@renderer/assets/images/apps/qwenlm.webp?url'
|
||||
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png?url'
|
||||
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png?url'
|
||||
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp?url'
|
||||
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png?url'
|
||||
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg?url'
|
||||
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png?url'
|
||||
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png?url'
|
||||
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png?url'
|
||||
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png?url'
|
||||
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png?url'
|
||||
import QwenModelLogo from '@renderer/assets/images/models/qwen.png?url'
|
||||
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png?url'
|
||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png?url'
|
||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png?url'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png?url'
|
||||
import BaiduAiAppLogo from '@renderer/assets/images/apps/baidu-ai.png'
|
||||
import BaicuanAppLogo from '@renderer/assets/images/apps/baixiaoying.webp'
|
||||
import BoltAppLogo from '@renderer/assets/images/apps/bolt.svg'
|
||||
import DevvAppLogo from '@renderer/assets/images/apps/devv.png'
|
||||
import DoubaoAppLogo from '@renderer/assets/images/apps/doubao.png'
|
||||
import DuckDuckGoAppLogo from '@renderer/assets/images/apps/duckduckgo.webp'
|
||||
import FeloAppLogo from '@renderer/assets/images/apps/felo.png'
|
||||
import GeminiAppLogo from '@renderer/assets/images/apps/gemini.png'
|
||||
import GensparkLogo from '@renderer/assets/images/apps/genspark.jpg'
|
||||
import GithubCopilotLogo from '@renderer/assets/images/apps/github-copilot.webp'
|
||||
import HuggingChatLogo from '@renderer/assets/images/apps/huggingchat.svg'
|
||||
import KimiAppLogo from '@renderer/assets/images/apps/kimi.jpg'
|
||||
import MetasoAppLogo from '@renderer/assets/images/apps/metaso.webp'
|
||||
import NamiAiSearchLogo from '@renderer/assets/images/apps/nm.webp'
|
||||
import PerplexityAppLogo from '@renderer/assets/images/apps/perplexity.webp'
|
||||
import PoeAppLogo from '@renderer/assets/images/apps/poe.webp'
|
||||
import ZhipuProviderLogo from '@renderer/assets/images/apps/qingyan.png'
|
||||
import SensetimeAppLogo from '@renderer/assets/images/apps/sensetime.png'
|
||||
import SparkDeskAppLogo from '@renderer/assets/images/apps/sparkdesk.png'
|
||||
import ThinkAnyLogo from '@renderer/assets/images/apps/thinkany.webp'
|
||||
import TiangongAiLogo from '@renderer/assets/images/apps/tiangong.png'
|
||||
import WanZhiAppLogo from '@renderer/assets/images/apps/wanzhi.jpg'
|
||||
import TencentYuanbaoAppLogo from '@renderer/assets/images/apps/yuanbao.png'
|
||||
import YuewenAppLogo from '@renderer/assets/images/apps/yuewen.png'
|
||||
import ZhihuAppLogo from '@renderer/assets/images/apps/zhihu.png'
|
||||
import ClaudeAppLogo from '@renderer/assets/images/models/claude.png'
|
||||
import HailuoModelLogo from '@renderer/assets/images/models/hailuo.png'
|
||||
import QwenModelLogo from '@renderer/assets/images/models/qwen.png'
|
||||
import DeepSeekProviderLogo from '@renderer/assets/images/providers/deepseek.png'
|
||||
import GroqProviderLogo from '@renderer/assets/images/providers/groq.png'
|
||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
|
||||
export const DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
const _apps: MinAppType[] = [
|
||||
{
|
||||
id: 'openai',
|
||||
name: 'ChatGPT',
|
||||
@@ -230,13 +226,6 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
url: 'https://thinkany.ai/',
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'hika',
|
||||
name: 'Hika',
|
||||
logo: HikaLogo,
|
||||
url: 'https://hika.fyi/',
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'github-copilot',
|
||||
name: 'GitHub Copilot',
|
||||
@@ -248,30 +237,14 @@ export const DEFAULT_MIN_APPS: MinAppType[] = [
|
||||
name: 'Genspark',
|
||||
logo: GensparkLogo,
|
||||
url: 'https://www.genspark.ai/'
|
||||
},
|
||||
{
|
||||
id: 'grok',
|
||||
name: 'Grok',
|
||||
logo: GrokAppLogo,
|
||||
url: 'https://x.com/i/grok',
|
||||
bodered: true
|
||||
},
|
||||
{
|
||||
id: 'qwenlm',
|
||||
name: 'QwenLM',
|
||||
logo: QwenlmAppLogo,
|
||||
url: 'https://qwenlm.ai/'
|
||||
},
|
||||
{
|
||||
id: 'flowith',
|
||||
name: 'Flowith',
|
||||
logo: FlowithAppLogo,
|
||||
url: 'https://www.flowith.io/',
|
||||
bodered: true
|
||||
}
|
||||
]
|
||||
|
||||
export function getAllMinApps() {
|
||||
return _apps as MinAppType[]
|
||||
}
|
||||
|
||||
export function startMinAppById(id: string) {
|
||||
const app = DEFAULT_MIN_APPS.find((app) => app?.id === id)
|
||||
const app = getAllMinApps().find((app) => app?.id === id)
|
||||
app && MinApp.start(app)
|
||||
}
|
||||
|
||||
@@ -125,8 +125,6 @@ import { getProviderByModel } from '@renderer/services/AssistantService'
|
||||
import { Model } from '@renderer/types'
|
||||
import OpenAI from 'openai'
|
||||
|
||||
import { getWebSearchTools } from './tools'
|
||||
|
||||
const visionAllowedModels = [
|
||||
'llava',
|
||||
'moondream',
|
||||
@@ -264,94 +262,6 @@ export function getModelLogo(modelId: string) {
|
||||
}
|
||||
|
||||
export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
qwenlm: [
|
||||
{
|
||||
id: 'qwen-plus-latest',
|
||||
provider: 'qwenlm',
|
||||
name: 'Qwen2.5-Plus',
|
||||
group: 'Qwen 2.5'
|
||||
},
|
||||
{
|
||||
id: 'qvq-72b-preview',
|
||||
provider: 'qwenlm',
|
||||
name: 'QVQ-72B-Preview',
|
||||
group: 'QVQ'
|
||||
},
|
||||
{
|
||||
id: 'qwq-32b-preview',
|
||||
provider: 'qwenlm',
|
||||
name: 'QwQ-32B-Preview',
|
||||
group: 'QVQ'
|
||||
},
|
||||
{
|
||||
id: 'qwen2.5-coder-32b-instruct',
|
||||
provider: 'qwenlm',
|
||||
name: 'Qwen2.5-Coder-32B-Instruct',
|
||||
group: 'Qwen 2.5'
|
||||
},
|
||||
{
|
||||
id: 'qwen-vl-max-latest',
|
||||
provider: 'qwenlm',
|
||||
name: 'Qwen2-VL-Max',
|
||||
group: 'Qwen 2'
|
||||
},
|
||||
{
|
||||
id: 'qwen-turbo-latest',
|
||||
provider: 'qwenlm',
|
||||
name: 'Qwen2.5-Turbo',
|
||||
group: 'Qwen 2.5'
|
||||
},
|
||||
{
|
||||
id: 'qwen2.5-72b-instruct',
|
||||
provider: 'qwenlm',
|
||||
name: 'Qwen2.5-72B-Instruct',
|
||||
group: 'Qwen 2.5'
|
||||
},
|
||||
{
|
||||
id: 'qwen2.5-32b-instruct',
|
||||
provider: 'qwenlm',
|
||||
name: 'Qwen2.5-32B-Instruct',
|
||||
group: 'Qwen 2.5'
|
||||
}
|
||||
],
|
||||
aihubmix: [
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
provider: 'aihubmix',
|
||||
name: 'GPT-4o',
|
||||
group: 'GPT-4o'
|
||||
},
|
||||
{
|
||||
id: 'claude-3-5-sonnet-latest',
|
||||
provider: 'aihubmix',
|
||||
name: 'Claude 3.5 Sonnet',
|
||||
group: 'Claude 3.5'
|
||||
},
|
||||
{
|
||||
id: 'gemini-2.0-flash-exp-search',
|
||||
provider: 'aihubmix',
|
||||
name: 'Gemini 2.0 Flash Exp Search',
|
||||
group: 'Gemini 2.0'
|
||||
},
|
||||
{
|
||||
id: 'deepseek-chat',
|
||||
provider: 'aihubmix',
|
||||
name: 'DeepSeek Chat',
|
||||
group: 'DeepSeek Chat'
|
||||
},
|
||||
{
|
||||
id: 'aihubmix-Llama-3-3-70B-Instruct',
|
||||
provider: 'aihubmix',
|
||||
name: 'Llama-3.3-70b',
|
||||
group: 'Llama 3.3'
|
||||
},
|
||||
{
|
||||
id: 'Qwen/QVQ-72B-Preview',
|
||||
provider: 'aihubmix',
|
||||
name: 'Qwen/QVQ-72B',
|
||||
group: 'Qwen'
|
||||
}
|
||||
],
|
||||
ollama: [],
|
||||
silicon: [
|
||||
{
|
||||
@@ -364,7 +274,7 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
id: 'Qwen/Qwen2.5-7B-Instruct',
|
||||
provider: 'silicon',
|
||||
name: 'Qwen2.5-7B-Instruct',
|
||||
group: 'Qwen'
|
||||
group: 'Qwen2.5'
|
||||
},
|
||||
{
|
||||
id: 'meta-llama/Llama-3.3-70B-Instruct',
|
||||
@@ -611,21 +521,9 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
],
|
||||
zhipu: [
|
||||
{
|
||||
id: 'glm-zero-preview',
|
||||
id: 'glm-4',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-Zero-Preview',
|
||||
group: 'GLM-Zero'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-0520',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-0520',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-long',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-Long',
|
||||
name: 'GLM-4',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
@@ -652,12 +550,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
name: 'GLM-4-Flash',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4-flashx',
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-FlashX',
|
||||
group: 'GLM-4'
|
||||
},
|
||||
{
|
||||
id: 'glm-4v',
|
||||
provider: 'zhipu',
|
||||
@@ -675,12 +567,6 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
provider: 'zhipu',
|
||||
name: 'GLM-4-AllTools',
|
||||
group: 'GLM-4-AllTools'
|
||||
},
|
||||
{
|
||||
id: 'embedding-3',
|
||||
provider: 'zhipu',
|
||||
name: 'Embedding-3',
|
||||
group: 'Embedding'
|
||||
}
|
||||
],
|
||||
moonshot: [
|
||||
@@ -864,6 +750,20 @@ export const SYSTEM_MODELS: Record<string, Model[]> = {
|
||||
group: 'Jina Embeddings V3'
|
||||
}
|
||||
],
|
||||
aihubmix: [
|
||||
{
|
||||
id: 'gpt-4o-mini',
|
||||
provider: 'aihubmix',
|
||||
name: 'GPT-4o Mini',
|
||||
group: 'GPT-4o'
|
||||
},
|
||||
{
|
||||
id: 'aihubmix-Llama-3-70B-Instruct',
|
||||
provider: 'aihubmix',
|
||||
name: 'Llama 3 70B Instruct',
|
||||
group: 'Llama3'
|
||||
}
|
||||
],
|
||||
fireworks: [
|
||||
{
|
||||
id: 'accounts/fireworks/models/mythomax-l2-13b',
|
||||
@@ -1061,11 +961,6 @@ export const TEXT_TO_IMAGES_MODELS = [
|
||||
}
|
||||
]
|
||||
|
||||
export const TEXT_TO_IMAGES_MODELS_SUPPORT_IMAGE_ENHANCEMENT = [
|
||||
'stabilityai/stable-diffusion-2-1',
|
||||
'stabilityai/stable-diffusion-xl-base-1.0'
|
||||
]
|
||||
|
||||
export function isTextToImageModel(model: Model): boolean {
|
||||
return TEXT_TO_IMAGE_REGEX.test(model.id)
|
||||
}
|
||||
@@ -1109,43 +1004,5 @@ export function isWebSearchModel(model: Model): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (provider?.type === 'openai') {
|
||||
if (model?.id?.includes('gemini-2.0-flash-exp')) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (provider.id === 'gemini' || provider?.type === 'gemini') {
|
||||
return model?.id === 'gemini-2.0-flash-exp'
|
||||
}
|
||||
|
||||
if (provider.id === 'hunyuan') {
|
||||
return model?.id !== 'hunyuan-lite'
|
||||
}
|
||||
|
||||
if (provider.id === 'aihubmix') {
|
||||
return model?.id === 'gemini-2.0-flash-exp-search'
|
||||
}
|
||||
|
||||
if (provider.id === 'zhipu') {
|
||||
return model?.id?.startsWith('glm-4-')
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function getOpenAIWebSearchParams(model: Model): Record<string, any> {
|
||||
if (isWebSearchModel(model)) {
|
||||
const webSearchTools = getWebSearchTools(model)
|
||||
|
||||
if (model.provider === 'hunyuan') {
|
||||
return { enable_enhancement: true }
|
||||
}
|
||||
|
||||
return {
|
||||
tools: webSearchTools
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
return (provider.id === 'gemini' || provider?.type === 'gemini') && model?.id === 'gemini-2.0-flash-exp'
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import OcoolAiProviderLogo from '@renderer/assets/images/providers/ocoolai.png'
|
||||
import OllamaProviderLogo from '@renderer/assets/images/providers/ollama.png'
|
||||
import OpenAiProviderLogo from '@renderer/assets/images/providers/openai.png'
|
||||
import OpenRouterProviderLogo from '@renderer/assets/images/providers/openrouter.png'
|
||||
import QwenLMProviderLogo from '@renderer/assets/images/providers/qwenlm.png'
|
||||
import SiliconFlowProviderLogo from '@renderer/assets/images/providers/silicon.png'
|
||||
import StepProviderLogo from '@renderer/assets/images/providers/step.png'
|
||||
import TogetherProviderLogo from '@renderer/assets/images/providers/together.png'
|
||||
@@ -92,8 +91,6 @@ export function getProviderLogo(providerId: string) {
|
||||
return MistralProviderLogo
|
||||
case 'jina':
|
||||
return JinaProviderLogo
|
||||
case 'qwenlm':
|
||||
return QwenLMProviderLogo
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
@@ -405,7 +402,7 @@ export const PROVIDER_CONFIG = {
|
||||
url: 'https://integrate.api.nvidia.com'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://build.nvidia.com/explore/discover',
|
||||
official: 'https://ai.360.com/',
|
||||
apiKey: 'https://build.nvidia.com/meta/llama-3_1-405b-instruct',
|
||||
docs: 'https://docs.api.nvidia.com/nim/reference/llm-apis',
|
||||
models: 'https://build.nvidia.com/nim'
|
||||
@@ -421,16 +418,5 @@ export const PROVIDER_CONFIG = {
|
||||
docs: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/',
|
||||
models: 'https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models'
|
||||
}
|
||||
},
|
||||
qwenlm: {
|
||||
api: {
|
||||
url: 'https://chat.qwenlm.ai/api/'
|
||||
},
|
||||
websites: {
|
||||
official: 'https://chat.qwenlm.ai',
|
||||
apiKey: 'https://chat.qwenlm.ai',
|
||||
docs: 'https://chat.qwenlm.ai',
|
||||
models: 'https://chat.qwenlm.ai'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Model } from '@renderer/types'
|
||||
import { ChatCompletionTool } from 'openai/resources'
|
||||
|
||||
export function getWebSearchTools(model: Model): ChatCompletionTool[] {
|
||||
if (model?.provider === 'zhipu') {
|
||||
if (model.id === 'glm-4-alltools') {
|
||||
return [
|
||||
{
|
||||
type: 'web_browser'
|
||||
} as unknown as ChatCompletionTool
|
||||
]
|
||||
}
|
||||
return [
|
||||
{
|
||||
type: 'web_search',
|
||||
web_search: {
|
||||
enable: true,
|
||||
search_result: true
|
||||
}
|
||||
} as unknown as ChatCompletionTool
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'googleSearch'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
export const TranslateLanguageOptions = [
|
||||
{
|
||||
value: 'english',
|
||||
label: i18n.t('languages.english'),
|
||||
emoji: '🇬🇧'
|
||||
},
|
||||
{
|
||||
value: 'chinese',
|
||||
label: i18n.t('languages.chinese'),
|
||||
emoji: '🇨🇳'
|
||||
},
|
||||
{
|
||||
value: 'chinese-traditional',
|
||||
label: i18n.t('languages.chinese-traditional'),
|
||||
emoji: '🇭🇰'
|
||||
},
|
||||
{
|
||||
value: 'japanese',
|
||||
label: i18n.t('languages.japanese'),
|
||||
emoji: '🇯🇵'
|
||||
},
|
||||
{
|
||||
value: 'korean',
|
||||
label: i18n.t('languages.korean'),
|
||||
emoji: '🇰🇷'
|
||||
},
|
||||
{
|
||||
value: 'russian',
|
||||
label: i18n.t('languages.russian'),
|
||||
emoji: '🇷🇺'
|
||||
},
|
||||
{
|
||||
value: 'spanish',
|
||||
label: i18n.t('languages.spanish'),
|
||||
emoji: '🇪🇸'
|
||||
},
|
||||
{
|
||||
value: 'french',
|
||||
label: i18n.t('languages.french'),
|
||||
emoji: '🇫🇷'
|
||||
},
|
||||
{
|
||||
value: 'italian',
|
||||
label: i18n.t('languages.italian'),
|
||||
emoji: '🇮🇹'
|
||||
},
|
||||
{
|
||||
value: 'portuguese',
|
||||
label: i18n.t('languages.portuguese'),
|
||||
emoji: '🇵🇹'
|
||||
},
|
||||
{
|
||||
value: 'arabic',
|
||||
label: i18n.t('languages.arabic'),
|
||||
emoji: '🇸🇦'
|
||||
}
|
||||
]
|
||||
@@ -54,12 +54,13 @@ export const SyntaxHighlighterProvider: React.FC<PropsWithChildren> = ({ childre
|
||||
const codeToHtml = async (code: string, language: string) => {
|
||||
if (!highlighter) return ''
|
||||
|
||||
const escapedCode = code?.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
||||
const escapedCode = code.replace(/[<>]/g, (char) => ({ '<': '<', '>': '>' })[char]!)
|
||||
|
||||
try {
|
||||
if (!highlighter.getLoadedLanguages().includes(language as BundledLanguage)) {
|
||||
if (language in bundledLanguages || language === 'text') {
|
||||
await highlighter.loadLanguage(language as BundledLanguage)
|
||||
console.log(`Loaded language: ${language}`)
|
||||
} else {
|
||||
return `<pre style="padding: 10px"><code>${escapedCode}</code></pre>`
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { isMac } from '@renderer/config/constant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
import { isMiniWindow } from '@renderer/utils'
|
||||
import React, { createContext, PropsWithChildren, useContext, useEffect, useState } from 'react'
|
||||
|
||||
interface ThemeContextType {
|
||||
@@ -14,11 +13,7 @@ const ThemeContext = createContext<ThemeContextType>({
|
||||
toggleTheme: () => {}
|
||||
})
|
||||
|
||||
interface ThemeProviderProps extends PropsWithChildren {
|
||||
defaultTheme?: ThemeMode
|
||||
}
|
||||
|
||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultTheme }) => {
|
||||
export const ThemeProvider: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const { theme, setTheme } = useSettings()
|
||||
const [_theme, _setTheme] = useState(theme)
|
||||
|
||||
@@ -27,7 +22,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultT
|
||||
}
|
||||
|
||||
useEffect((): any => {
|
||||
if (theme === ThemeMode.auto || defaultTheme === ThemeMode.auto) {
|
||||
if (theme === ThemeMode.auto) {
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
_setTheme(mediaQuery.matches ? ThemeMode.dark : ThemeMode.light)
|
||||
const handleChange = (e: MediaQueryListEvent) => _setTheme(e.matches ? ThemeMode.dark : ThemeMode.light)
|
||||
@@ -36,13 +31,11 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children, defaultT
|
||||
} else {
|
||||
_setTheme(theme)
|
||||
}
|
||||
}, [defaultTheme, theme])
|
||||
}, [theme])
|
||||
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('theme-mode', _theme)
|
||||
if (!isMiniWindow()) {
|
||||
window.api?.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light')
|
||||
}
|
||||
window.api?.setTheme(_theme === ThemeMode.dark ? 'dark' : 'light')
|
||||
}, [_theme])
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { isMac } from '@renderer/config/constant'
|
||||
import { isLocalAi } from '@renderer/config/env'
|
||||
import db from '@renderer/databases'
|
||||
import i18n from '@renderer/i18n'
|
||||
import { startAutoSync, stopAutoSync } from '@renderer/services/BackupService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setAvatar, setFilesPath, setUpdateState } from '@renderer/store/runtime'
|
||||
import { delay, runAsyncFunction } from '@renderer/utils'
|
||||
@@ -15,7 +16,16 @@ import useUpdateHandler from './useUpdateHandler'
|
||||
|
||||
export function useAppInit() {
|
||||
const dispatch = useAppDispatch()
|
||||
const { proxyUrl, language, windowStyle, manualUpdateCheck, proxyMode, customCss } = useSettings()
|
||||
const {
|
||||
proxyUrl,
|
||||
language,
|
||||
windowStyle,
|
||||
manualUpdateCheck,
|
||||
proxyMode,
|
||||
webdavAutoSync,
|
||||
webdavSyncInterval,
|
||||
customCss
|
||||
} = useSettings()
|
||||
const { minappShow } = useRuntime()
|
||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||
@@ -74,6 +84,10 @@ export function useAppInit() {
|
||||
})
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
webdavAutoSync ? startAutoSync() : stopAutoSync()
|
||||
}, [webdavAutoSync, webdavSyncInterval])
|
||||
|
||||
useEffect(() => {
|
||||
import('@renderer/queue/KnowledgeQueue')
|
||||
}, [])
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
renameBase,
|
||||
updateBase,
|
||||
updateBases,
|
||||
updateItem as updateItemAction,
|
||||
updateItemProcessingStatus,
|
||||
updateNotes
|
||||
} from '@renderer/store/knowledge'
|
||||
@@ -118,8 +117,7 @@ export const useKnowledge = (baseId: string) => {
|
||||
await db.knowledge_notes.put(updatedNote)
|
||||
dispatch(updateNotes({ baseId, item: updatedNote }))
|
||||
}
|
||||
const noteItem = base?.items.find((item) => item.id === noteId)
|
||||
noteItem && refreshItem(noteItem)
|
||||
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||
}
|
||||
|
||||
// 获取笔记内容
|
||||
@@ -127,10 +125,6 @@ export const useKnowledge = (baseId: string) => {
|
||||
return await db.knowledge_notes.get(noteId)
|
||||
}
|
||||
|
||||
const updateItem = (item: KnowledgeItem) => {
|
||||
dispatch(updateItemAction({ baseId, item }))
|
||||
}
|
||||
|
||||
// 移除项目
|
||||
const removeItem = async (item: KnowledgeItem) => {
|
||||
dispatch(removeItemAction({ baseId, item }))
|
||||
@@ -144,27 +138,6 @@ export const useKnowledge = (baseId: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新项目
|
||||
const refreshItem = async (item: KnowledgeItem) => {
|
||||
const status = getProcessingStatus(item.id)
|
||||
|
||||
if (status === 'pending' || status === 'processing') {
|
||||
return
|
||||
}
|
||||
|
||||
if (base && item.uniqueId) {
|
||||
await window.api.knowledgeBase.remove({ uniqueId: item.uniqueId, base: getKnowledgeBaseParams(base) })
|
||||
updateItem({
|
||||
...item,
|
||||
processingStatus: 'pending',
|
||||
processingProgress: 0,
|
||||
processingError: '',
|
||||
uniqueId: undefined
|
||||
})
|
||||
setTimeout(() => KnowledgeQueue.checkAllBases(), 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新处理状态
|
||||
const updateItemStatus = (itemId: string, status: ProcessingStatus, progress?: number, error?: string) => {
|
||||
dispatch(
|
||||
@@ -265,9 +238,7 @@ export const useKnowledge = (baseId: string) => {
|
||||
addNote,
|
||||
updateNoteContent,
|
||||
getNoteContent,
|
||||
updateItem,
|
||||
updateItemStatus,
|
||||
refreshItem,
|
||||
getProcessingStatus,
|
||||
getProcessingItemsByType,
|
||||
clearCompleted,
|
||||
|
||||
@@ -37,32 +37,4 @@ export const useMermaid = () => {
|
||||
|
||||
setTimeout(renderMermaid, 100)
|
||||
}, [generating])
|
||||
|
||||
useEffect(() => {
|
||||
const handleWheel = (e: WheelEvent) => {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
e.preventDefault()
|
||||
const mermaidElement = (e.target as HTMLElement).closest('.mermaid')
|
||||
if (!mermaidElement) return
|
||||
|
||||
const svg = mermaidElement.querySelector('svg')
|
||||
if (!svg) return
|
||||
|
||||
const currentScale = parseFloat(svg.style.transform?.match(/scale\((.*?)\)/)?.[1] || '1')
|
||||
const delta = e.deltaY < 0 ? 0.1 : -0.1
|
||||
const newScale = Math.max(0.1, Math.min(3, currentScale + delta))
|
||||
|
||||
const container = svg.parentElement
|
||||
if (container) {
|
||||
container.style.overflow = 'auto'
|
||||
container.style.position = 'relative'
|
||||
svg.style.transformOrigin = 'top left'
|
||||
svg.style.transform = `scale(${newScale})`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('wheel', handleWheel, { passive: false })
|
||||
return () => document.removeEventListener('wheel', handleWheel)
|
||||
}, [])
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||
import { RootState, useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setDisabledMinApps, setMinApps, setPinnedMinApps } from '@renderer/store/minapps'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
|
||||
export const useMinapps = () => {
|
||||
const { enabled, disabled, pinned } = useAppSelector((state: RootState) => state.minapps)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
return {
|
||||
minapps: enabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
disabled: disabled.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
pinned: pinned.map((app) => DEFAULT_MIN_APPS.find((item) => item.id === app.id) || app),
|
||||
updateMinapps: (minapps: MinAppType[]) => {
|
||||
dispatch(setMinApps(minapps))
|
||||
},
|
||||
updateDisabledMinapps: (minapps: MinAppType[]) => {
|
||||
dispatch(setDisabledMinApps(minapps))
|
||||
},
|
||||
updatePinnedMinapps: (minapps: MinAppType[]) => {
|
||||
dispatch(setPinnedMinApps(minapps))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ export function usePaintings() {
|
||||
paintings,
|
||||
addPainting: () => {
|
||||
const newPainting: Painting = {
|
||||
model: TEXT_TO_IMAGES_MODELS[0].id,
|
||||
id: uuid(),
|
||||
urls: [],
|
||||
files: [],
|
||||
@@ -25,7 +24,7 @@ export function usePaintings() {
|
||||
seed: generateRandomSeed(),
|
||||
steps: 25,
|
||||
guidanceScale: 4.5,
|
||||
promptEnhancement: true
|
||||
model: TEXT_TO_IMAGES_MODELS[0].id
|
||||
}
|
||||
dispatch(addPainting(newPainting))
|
||||
return newPainting
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import store, { useAppSelector } from '@renderer/store'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
|
||||
export function useRuntime() {
|
||||
return useAppSelector((state) => state.runtime)
|
||||
}
|
||||
|
||||
export function modelGenerating() {
|
||||
const generating = store.getState().runtime.generating
|
||||
|
||||
if (generating) {
|
||||
window.message.warning({ content: i18n.t('message.switch.disabled'), key: 'model-generating' })
|
||||
return Promise.reject()
|
||||
}
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
@@ -2,14 +2,13 @@ import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import {
|
||||
SendMessageShortcut,
|
||||
setSendMessageShortcut as _setSendMessageShortcut,
|
||||
setSidebarIcons,
|
||||
setTheme,
|
||||
SettingsState,
|
||||
setTopicPosition,
|
||||
setTray,
|
||||
setWindowStyle
|
||||
} from '@renderer/store/settings'
|
||||
import { SidebarIcon, ThemeMode } from '@renderer/types'
|
||||
import { ThemeMode } from '@renderer/types'
|
||||
|
||||
export function useSettings() {
|
||||
const settings = useAppSelector((state) => state.settings)
|
||||
@@ -22,7 +21,6 @@ export function useSettings() {
|
||||
},
|
||||
setTray(isActive: boolean) {
|
||||
dispatch(setTray(isActive))
|
||||
window.api.setTray(isActive)
|
||||
},
|
||||
setTheme(theme: ThemeMode) {
|
||||
dispatch(setTheme(theme))
|
||||
@@ -32,15 +30,6 @@ export function useSettings() {
|
||||
},
|
||||
setTopicPosition(topicPosition: 'left' | 'right') {
|
||||
dispatch(setTopicPosition(topicPosition))
|
||||
},
|
||||
updateSidebarIcons(icons: { visible: SidebarIcon[]; disabled: SidebarIcon[] }) {
|
||||
dispatch(setSidebarIcons(icons))
|
||||
},
|
||||
updateSidebarVisibleIcons(icons: SidebarIcon[]) {
|
||||
dispatch(setSidebarIcons({ visible: icons }))
|
||||
},
|
||||
updateSidebarDisabledIcons(icons: SidebarIcon[]) {
|
||||
dispatch(setSidebarIcons({ disabled: icons }))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { orderBy } from 'lodash'
|
||||
import { useCallback } from 'react'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
|
||||
@@ -59,7 +58,7 @@ export const useShortcut = (
|
||||
|
||||
export function useShortcuts() {
|
||||
const shortcuts = useAppSelector((state) => state.shortcuts.shortcuts)
|
||||
return { shortcuts: orderBy(shortcuts, 'system', 'desc') }
|
||||
return { shortcuts }
|
||||
}
|
||||
|
||||
export function useShortcutDisplay(key: string) {
|
||||
|
||||
@@ -9,10 +9,7 @@ export default function useUpdateHandler() {
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
if (!window.electron) return
|
||||
|
||||
const ipcRenderer = window.electron.ipcRenderer
|
||||
|
||||
const removers = [
|
||||
ipcRenderer.on('update-not-available', () => {
|
||||
dispatch(setUpdateState({ checking: false }))
|
||||
|
||||
@@ -64,14 +64,14 @@
|
||||
"default.description": "Hello, I'm Default Assistant. You can start chatting with me right away",
|
||||
"default.name": "⭐️ Default Assistant",
|
||||
"default.topic.name": "Default Topic",
|
||||
"input.clear": "Clear {{Command}}",
|
||||
"input.clear": "Clear",
|
||||
"input.clear.content": "Do you want to clear all messages of the current topic?",
|
||||
"input.clear.title": "Clear all messages?",
|
||||
"input.collapse": "Collapse",
|
||||
"input.context_count.tip": "Context Count",
|
||||
"input.estimated_tokens.tip": "Estimated tokens",
|
||||
"input.expand": "Expand",
|
||||
"input.new.context": "Clear Context {{Command}}",
|
||||
"input.new.context": "Clear Context",
|
||||
"input.new_topic": "New Topic {{Command}}",
|
||||
"input.pause": "Pause",
|
||||
"input.placeholder": "Type your message here...",
|
||||
@@ -183,14 +183,8 @@
|
||||
"name": "Name",
|
||||
"open": "Open",
|
||||
"size": "Size",
|
||||
"type": "Type",
|
||||
"text": "Text",
|
||||
"title": "Files",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"delete.title": "Delete File",
|
||||
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete this file?",
|
||||
"delete.paintings.warning": "Image contains this file, deletion is not possible"
|
||||
"title": "Files"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "Continue Chatting",
|
||||
@@ -218,10 +212,6 @@
|
||||
"png": "Download PNG",
|
||||
"svg": "Download SVG"
|
||||
},
|
||||
"resize": {
|
||||
"zoom-in": "Zoom In",
|
||||
"zoom-out": "Zoom Out"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "Preview",
|
||||
"source": "Source"
|
||||
@@ -231,7 +221,6 @@
|
||||
"message": {
|
||||
"api.connection.failed": "Connection failed",
|
||||
"api.connection.success": "Connection successful",
|
||||
"api.check.model.title": "Select the model to use for detection",
|
||||
"assistant.added.content": "Assistant added successfully",
|
||||
"backup.failed": "Backup failed",
|
||||
"backup.success": "Backup successful",
|
||||
@@ -255,7 +244,7 @@
|
||||
"reset.double.confirm.title": "DATA LOST !!!",
|
||||
"restore.success": "Restored successfully",
|
||||
"save.success.title": "Saved successfully",
|
||||
"switch.disabled": "Please wait for the current reply to complete",
|
||||
"switch.disabled": "Switching is disabled while the assistant is generating",
|
||||
"topic.added": "New topic added",
|
||||
"upgrade.success.button": "Restart",
|
||||
"upgrade.success.content": "Please restart the application to complete the upgrade",
|
||||
@@ -265,9 +254,7 @@
|
||||
"error.get_embedding_dimensions": "Failed to get embedding dimensions"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "MinApp",
|
||||
"sidebar.add.title": "Add to sidebar",
|
||||
"sidebar.remove.title": "Remove from sidebar"
|
||||
"title": "MinApp"
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
||||
@@ -292,9 +279,7 @@
|
||||
"regenerate.confirm": "This will replace your existing generated images. Do you want to continue?",
|
||||
"seed": "Seed",
|
||||
"seed_tip": "The same seed and prompt can produce similar images",
|
||||
"title": "Images",
|
||||
"prompt_enhancement": "Prompt Enhancement",
|
||||
"prompt_enhancement_tip": "Rewrite prompts into detailed, model-friendly versions when switched on"
|
||||
"title": "Images"
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
@@ -326,8 +311,7 @@
|
||||
"together": "Together",
|
||||
"yi": "Yi",
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI",
|
||||
"qwenlm": "QwenLM"
|
||||
"zhipu": "ZHIPU AI"
|
||||
},
|
||||
"settings": {
|
||||
"about": "About & Feedback",
|
||||
@@ -374,22 +358,11 @@
|
||||
"webdav.password": "WebDAV Password",
|
||||
"webdav.path": "WebDAV Path",
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.autoSync": "Auto Backup",
|
||||
"webdav.autoSync": "Auto Sync",
|
||||
"webdav.minutes": "Minutes",
|
||||
"webdav.restore.button": "Restore from WebDAV",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV User",
|
||||
"webdav.syncStatus": "Backup Status",
|
||||
"webdav.autoSync.off": "Off",
|
||||
"webdav.noSync": "Waiting for next backup",
|
||||
"webdav.syncError": "Backup Error",
|
||||
"webdav.lastSync": "Last Backup"
|
||||
},
|
||||
"quickAssistant": {
|
||||
"title": "Quick Assistant",
|
||||
"click_tray_to_show": "Click the tray icon to start",
|
||||
"enable_quick_assistant": "Enable Quick Assistant",
|
||||
"use_shortcut_to_show": "Right-click the tray icon or use shortcuts to start"
|
||||
"webdav.user": "WebDAV User"
|
||||
},
|
||||
"display.title": "Display Settings",
|
||||
"font_size.title": "Message font size",
|
||||
@@ -405,21 +378,9 @@
|
||||
"general.user_name.placeholder": "Enter your name",
|
||||
"general.view_webdav_settings": "View WebDAV settings",
|
||||
"general.display.title": "Display Settings",
|
||||
"display.sidebar.translate.icon": "Show Translate icon",
|
||||
"display.sidebar.painting.icon": "Show Painting icon",
|
||||
"display.sidebar.minapp.icon": "Show MinApp icon",
|
||||
"display.sidebar.knowledge.icon": "Show Knowledge icon",
|
||||
"display.sidebar.files.icon": "Show Files icon",
|
||||
"display.sidebar.title": "Sidebar Settings",
|
||||
"display.sidebar.visible": "Show icons",
|
||||
"display.sidebar.disabled": "Hide icons",
|
||||
"display.sidebar.chat.hiddenMessage": "Assistants are basic functions, not supported for hiding",
|
||||
"display.sidebar.empty": "Drag the hidden feature from the left side here",
|
||||
"display.minApp.title": "MinApp Settings",
|
||||
"display.minApp.visible": "Visible MinApp",
|
||||
"display.minApp.disabled": "Hidden MinApp",
|
||||
"display.minApp.empty": "Drag minApp from the left to hide them here",
|
||||
"": "MinApp that have been added to the sidebar do not support hiding. If you want to hide them, please remove them from the sidebar first.",
|
||||
"display.topic.title": "Topic Settings",
|
||||
"display.custom.css": "Custom CSS",
|
||||
"display.custom.css.placeholder": "/* Put custom CSS here */",
|
||||
@@ -430,7 +391,7 @@
|
||||
"messages.input.show_estimated_tokens": "Show estimated tokens",
|
||||
"messages.metrics": "{{time_first_token_millsec}}ms to first token | {{token_speed}} tok/sec",
|
||||
"messages.input.title": "Input Settings",
|
||||
"messages.markdown_rendering_input_message": "Markdown render input message",
|
||||
"messages.markdown_rendering_input_message": "Markdown render input msg",
|
||||
"messages.math_engine": "Math render engine",
|
||||
"messages.model.title": "Model Settings",
|
||||
"messages.title": "Message Settings",
|
||||
@@ -524,11 +485,7 @@
|
||||
"clear_shortcut": "Clear Shortcut",
|
||||
"toggle_show_assistants": "Toggle Assistants",
|
||||
"toggle_show_topics": "Toggle Topics",
|
||||
"copy_last_message": "Copy Last Message",
|
||||
"search_message": "Search Message",
|
||||
"mini_window": "Quick Assistant",
|
||||
"clear_topic": "Clear Messages",
|
||||
"toggle_new_context": "Clear Context"
|
||||
"copy_last_message": "Copy Last Message"
|
||||
},
|
||||
"theme.auto": "Auto",
|
||||
"theme.dark": "Dark",
|
||||
@@ -561,8 +518,7 @@
|
||||
},
|
||||
"tray": {
|
||||
"quit": "Quit",
|
||||
"show_window": "Show Window",
|
||||
"show_mini_window": "Quick Assistant"
|
||||
"show_window": "Show Window"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "Knowledge Graph",
|
||||
@@ -570,7 +526,7 @@
|
||||
"show_window": "Show Window",
|
||||
"quit": "Quit"
|
||||
},
|
||||
"knowledge": {
|
||||
"knowledge_base": {
|
||||
"title": "Knowledge Base",
|
||||
"search": "Search knowledge base",
|
||||
"empty": "No knowledge base found",
|
||||
@@ -612,7 +568,6 @@
|
||||
"directory_placeholder": "Enter Directory Path",
|
||||
"model_info": "Model Info",
|
||||
"not_support": "Knowledge base database engine updated, the knowledge base will no longer be supported, please create a new knowledge base",
|
||||
"no_provider": "Knowledge base model provider is not set, the knowledge base will no longer be supported, please create a new knowledge base",
|
||||
"source": "Source"
|
||||
},
|
||||
"models": {
|
||||
@@ -639,37 +594,11 @@
|
||||
"parameter_type": {
|
||||
"string": "Text",
|
||||
"number": "Number",
|
||||
"boolean": "Boolean",
|
||||
"json": "JSON"
|
||||
"boolean": "Boolean"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"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",
|
||||
"copy_last_message": "Press C to copy"
|
||||
}
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +64,14 @@
|
||||
"default.description": "こんにちは、私はデフォルトのアシスタントです。すぐにチャットを始められます。",
|
||||
"default.name": "⭐️ デフォルトアシスタント",
|
||||
"default.topic.name": "デフォルトトピック",
|
||||
"input.clear": "クリア {{Command}}",
|
||||
"input.clear": "クリア",
|
||||
"input.clear.content": "現在のトピックのすべてのメッセージをクリアしますか?",
|
||||
"input.clear.title": "すべてのメッセージをクリアしますか?",
|
||||
"input.collapse": "折りたたむ",
|
||||
"input.context_count.tip": "コンテキスト数",
|
||||
"input.estimated_tokens.tip": "推定トークン数",
|
||||
"input.expand": "展開",
|
||||
"input.new.context": "コンテキストをクリア {{Command}}",
|
||||
"input.new.context": "コンテキストをクリア",
|
||||
"input.new_topic": "新しいトピック {{Command}}",
|
||||
"input.pause": "一時停止",
|
||||
"input.placeholder": "ここにメッセージを入力...",
|
||||
@@ -183,14 +183,8 @@
|
||||
"name": "名前",
|
||||
"open": "開く",
|
||||
"size": "サイズ",
|
||||
"type": "タイプ",
|
||||
"text": "テキスト",
|
||||
"title": "ファイル",
|
||||
"edit": "編集",
|
||||
"delete": "削除",
|
||||
"delete.title": "ファイルを削除",
|
||||
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。このファイルを削除してもよろしいですか?",
|
||||
"delete.paintings.warning": "画像に含まれているため、削除できません"
|
||||
"title": "ファイル"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "チャットを続ける",
|
||||
@@ -218,10 +212,6 @@
|
||||
"png": "PNGをダウンロード",
|
||||
"svg": "SVGをダウンロード"
|
||||
},
|
||||
"resize": {
|
||||
"zoom-in": "拡大する",
|
||||
"zoom-out": "ズームアウト"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "プレビュー",
|
||||
"source": "ソース"
|
||||
@@ -231,7 +221,6 @@
|
||||
"message": {
|
||||
"api.connection.failed": "接続に失敗しました",
|
||||
"api.connection.success": "接続に成功しました",
|
||||
"api.check.model.title": "検出に使用するモデルを選択してください",
|
||||
"assistant.added.content": "アシスタントが追加されました",
|
||||
"backup.failed": "バックアップに失敗しました",
|
||||
"backup.success": "バックアップに成功しました",
|
||||
@@ -254,7 +243,7 @@
|
||||
"reset.double.confirm.title": "データが失われます!!!",
|
||||
"restore.success": "復元に成功しました",
|
||||
"save.success.title": "保存に成功しました",
|
||||
"switch.disabled": "現在の応答が完了するまで切り替えを無効にします",
|
||||
"switch.disabled": "アシスタントが生成中は切り替えが無効です",
|
||||
"topic.added": "新しいトピックが追加されました",
|
||||
"upgrade.success.button": "再起動",
|
||||
"upgrade.success.content": "アップグレードを完了するためにアプリケーションを再起動してください",
|
||||
@@ -263,9 +252,7 @@
|
||||
"copy.success": "コピーしました!"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "ミニアプリ",
|
||||
"sidebar.add.title": "サイドバーに追加",
|
||||
"sidebar.remove.title": "サイドバーから削除"
|
||||
"title": "ミニアプリ"
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
|
||||
@@ -290,9 +277,7 @@
|
||||
"regenerate.confirm": "これにより、既存の生成画像が置き換えられます。続行しますか?",
|
||||
"seed": "シード",
|
||||
"seed_tip": "同じシードとプロンプトで似た画像を生成できます",
|
||||
"title": "画像",
|
||||
"prompt_enhancement": "プロンプト強化",
|
||||
"prompt_enhancement_tip": "オンにすると、プロンプトを詳細でモデルに適したバージョンに書き直します"
|
||||
"title": "画像"
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
@@ -324,8 +309,7 @@
|
||||
"together": "Together",
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360智脳",
|
||||
"zhipu": "智譜AI",
|
||||
"qwenlm": "QwenLM"
|
||||
"zhipu": "智譜AI"
|
||||
},
|
||||
"settings": {
|
||||
"about": "について",
|
||||
@@ -372,22 +356,11 @@
|
||||
"webdav.password": "WebDAVパスワード",
|
||||
"webdav.path": "WebDAVパス",
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.autoSync": "自動バックアップ",
|
||||
"webdav.autoSync": "自動同期",
|
||||
"webdav.minutes": "分",
|
||||
"webdav.restore.button": "WebDAVから復元",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAVユーザー",
|
||||
"webdav.syncStatus": "バックアップ状態",
|
||||
"webdav.autoSync.off": "オフ",
|
||||
"webdav.noSync": "次回のバックアップを待っています",
|
||||
"webdav.syncError": "バックアップエラー",
|
||||
"webdav.lastSync": "最終同期"
|
||||
},
|
||||
"quickAssistant": {
|
||||
"title": "クイックアシスタント",
|
||||
"click_tray_to_show": "トレイアイコンをクリックして起動",
|
||||
"enable_quick_assistant": "クイックアシスタントを有効にする",
|
||||
"use_shortcut_to_show": "トレイアイコンを右クリックするか、ショートカットキーで起動できます"
|
||||
"webdav.user": "WebDAVユーザー"
|
||||
},
|
||||
"display.title": "表示設定",
|
||||
"font_size.title": "メッセージのフォントサイズ",
|
||||
@@ -403,23 +376,12 @@
|
||||
"general.user_name.placeholder": "ユーザー名を入力",
|
||||
"general.view_webdav_settings": "WebDAV設定を表示",
|
||||
"general.display.title": "表示設定",
|
||||
"display.sidebar.translate.icon": "翻訳のアイコンを表示",
|
||||
"display.sidebar.painting.icon": "絵画のアイコンを表示",
|
||||
"display.sidebar.minapp.icon": "ミニアプリのアイコンを表示",
|
||||
"display.sidebar.knowledge.icon": "ナレッジのアイコンを表示",
|
||||
"display.sidebar.files.icon": "ファイルのアイコンを表示",
|
||||
"display.sidebar.title": "サイドバー設定",
|
||||
"display.sidebar.visible": "アイコンを表示",
|
||||
"display.sidebar.disabled": "アイコンを非表示",
|
||||
"display.sidebar.chat.hiddenMessage": "アシスタントは基本的な機能であり、非表示はサポートされていません",
|
||||
"display.sidebar.empty": "非表示にする機能を左側からここにドラッグ",
|
||||
"display.topic.title": "トピック設定",
|
||||
"display.custom.css": "カスタムCSS",
|
||||
"display.custom.css.placeholder": "/* ここにカスタムCSSを入力 */",
|
||||
"display.minApp.title": "ミニプログラム表示設定",
|
||||
"display.minApp.visible": "表示中ミニプログラム",
|
||||
"display.minApp.disabled": "非表示ミニプログラム",
|
||||
"display.minApp.empty": "非表示にしたいアプレットを左からここまでドラッグします",
|
||||
"input.auto_translate_with_space": "スペースを3回押して翻訳",
|
||||
"messages.divider": "メッセージ間に区切り線を表示",
|
||||
"messages.input.paste_long_text_as_file": "長いテキストをファイルとして貼り付け",
|
||||
@@ -509,11 +471,7 @@
|
||||
"clear_shortcut": "ショートカットをクリア",
|
||||
"toggle_show_assistants": "アシスタントの表示を切り替え",
|
||||
"toggle_show_topics": "トピックの表示を切り替え",
|
||||
"copy_last_message": "最後のメッセージをコピー",
|
||||
"search_message": "メッセージを検索",
|
||||
"mini_window": "クイックアシスタント",
|
||||
"clear_topic": "メッセージを消去",
|
||||
"toggle_new_context": "コンテキストをクリア"
|
||||
"copy_last_message": "最後のメッセージをコピー"
|
||||
},
|
||||
"theme.auto": "自動",
|
||||
"theme.dark": "ダークテーマ",
|
||||
@@ -546,8 +504,7 @@
|
||||
},
|
||||
"tray": {
|
||||
"quit": "終了",
|
||||
"show_window": "ウィンドウを表示",
|
||||
"show_mini_window": "クイックアシスタント"
|
||||
"show_window": "ウィンドウを表示"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "ナレッジグラフ",
|
||||
@@ -555,7 +512,7 @@
|
||||
"show_window": "ウィンドウを表示",
|
||||
"quit": "終了"
|
||||
},
|
||||
"knowledge": {
|
||||
"knowledge_base": {
|
||||
"title": "ナレッジベース",
|
||||
"search": "ナレッジベースを検索",
|
||||
"empty": "ナレッジベースが見つかりません",
|
||||
@@ -594,11 +551,7 @@
|
||||
"sitemap_placeholder": "サイトマップURLを入力",
|
||||
"directories": "ディレクトリ",
|
||||
"add_directory": "ディレクトリを追加",
|
||||
"directory_placeholder": "ディレクトリパスを入力",
|
||||
"model_info": "モデル情報",
|
||||
"not_support": "ナレッジベースデータベースエンジンが更新されました。このナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
|
||||
"no_provider": "ナレッジベースモデルプロバイダーが設定されていません。ナレッジベースはもうサポートされていません。新しいナレッジベースを作成してください",
|
||||
"source": "ソース"
|
||||
"directory_placeholder": "ディレクトリパスを入力"
|
||||
},
|
||||
"models": {
|
||||
"pinned": "固定済み",
|
||||
@@ -624,37 +577,11 @@
|
||||
"parameter_type": {
|
||||
"string": "テキスト",
|
||||
"number": "数値",
|
||||
"boolean": "真偽値",
|
||||
"json": "JSON"
|
||||
"boolean": "真偽値"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"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": "戻る",
|
||||
"copy_last_message": "C キーを押してコピー"
|
||||
}
|
||||
"summarize": "あなたは会話を得意とするアシスタントです。ユーザーの会話を10文字以内のタイトルに要約し、ユーザーの主言語と一致していることを確認してください。句読点や特殊記号は使用しないでください。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +64,14 @@
|
||||
"default.description": "Привет, я Ассистент по умолчанию. Вы можете начать общаться со мной прямо сейчас",
|
||||
"default.name": "⭐️ Ассистент по умолчанию",
|
||||
"default.topic.name": "Топик по умолчанию",
|
||||
"input.clear": "Очистить {{Command}}",
|
||||
"input.clear": "Очистить",
|
||||
"input.clear.content": "Хотите очистить все сообщения текущего топика?",
|
||||
"input.clear.title": "Очистить все сообщения?",
|
||||
"input.collapse": "Свернуть",
|
||||
"input.context_count.tip": "Количество контекстов",
|
||||
"input.estimated_tokens.tip": "Затраты токенов",
|
||||
"input.expand": "Развернуть",
|
||||
"input.new.context": "Очистить контекст {{Command}}",
|
||||
"input.new.context": "Очистить контекст",
|
||||
"input.new_topic": "Новый топик {{Command}}",
|
||||
"input.pause": "Остановить",
|
||||
"input.placeholder": "Введите ваше сообщение здесь...",
|
||||
@@ -183,14 +183,8 @@
|
||||
"name": "Имя",
|
||||
"open": "Открыть",
|
||||
"size": "Размер",
|
||||
"type": "Тип",
|
||||
"text": "Текст",
|
||||
"title": "Файлы",
|
||||
"edit": "Редактировать",
|
||||
"delete": "Удалить",
|
||||
"delete.title": "Удалить файл",
|
||||
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот файл?",
|
||||
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно"
|
||||
"title": "Файлы"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "Продолжить чат",
|
||||
@@ -218,10 +212,6 @@
|
||||
"png": "Скачать PNG",
|
||||
"svg": "Скачать SVG"
|
||||
},
|
||||
"resize": {
|
||||
"zoom-in": "Yвеличить",
|
||||
"zoom-out": "Yменьшить масштаб"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "Предпросмотр",
|
||||
"source": "Исходный код"
|
||||
@@ -231,7 +221,6 @@
|
||||
"message": {
|
||||
"api.connection.failed": "Соединение не удалось",
|
||||
"api.connection.success": "Соединение успешно",
|
||||
"api.check.model.title": "Выберите модель для проверки",
|
||||
"assistant.added.content": "Ассистент успешно добавлен",
|
||||
"backup.failed": "Создание резервной копии не удалось",
|
||||
"backup.success": "Резервная копия успешно создана",
|
||||
@@ -255,7 +244,7 @@
|
||||
"reset.double.confirm.title": "ДАННЫЕ БУДУТ УТЕРЯНЫ !!!",
|
||||
"restore.success": "Успешно восстановлено",
|
||||
"save.success.title": "Успешно сохранено",
|
||||
"switch.disabled": "Пожалуйста, дождитесь завершения текущего ответа",
|
||||
"switch.disabled": "Переключение отключено, пока ассистент генерирует",
|
||||
"topic.added": "Новый топик добавлен",
|
||||
"upgrade.success.button": "Перезапустить",
|
||||
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
|
||||
@@ -265,9 +254,7 @@
|
||||
"error.get_embedding_dimensions": "Не удалось получить размерность встраивания"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "Встроенные приложения",
|
||||
"sidebar.add.title": "Добавить в боковую панель",
|
||||
"sidebar.remove.title": "Удалить из боковой панели"
|
||||
"title": "Встроенные приложения"
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
||||
@@ -292,9 +279,7 @@
|
||||
"regenerate.confirm": "Это заменит ваши существующие сгенерированные изображения. Хотите продолжить?",
|
||||
"seed": "Ключ генерации",
|
||||
"seed_tip": "Одинаковый ключ генерации и промпт могут производить похожие изображения",
|
||||
"title": "Изображения",
|
||||
"prompt_enhancement": "Улучшение промпта",
|
||||
"prompt_enhancement_tip": "При включении переписывает промпт в более детальную, модель-ориентированную версию"
|
||||
"title": "Изображения"
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
@@ -326,8 +311,7 @@
|
||||
"together": "Together",
|
||||
"yi": "Yi",
|
||||
"zhinao": "360AI",
|
||||
"zhipu": "ZHIPU AI",
|
||||
"qwenlm": "QwenLM"
|
||||
"zhipu": "ZHIPU AI"
|
||||
},
|
||||
"settings": {
|
||||
"about": "О программе и обратная связь",
|
||||
@@ -374,22 +358,11 @@
|
||||
"webdav.password": "Пароль WebDAV",
|
||||
"webdav.path": "Путь WebDAV",
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.autoSync": "Автоматическое резервное копирование",
|
||||
"webdav.autoSync": "Автоматическая синхронизация",
|
||||
"webdav.minutes": "минут",
|
||||
"webdav.restore.button": "Восстановление с WebDAV",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "Пользователь WebDAV",
|
||||
"webdav.syncStatus": "Статус резервного копирования",
|
||||
"webdav.autoSync.off": "Выключено",
|
||||
"webdav.noSync": "Ожидание следующего резервного копирования",
|
||||
"webdav.syncError": "Ошибка резервного копирования",
|
||||
"webdav.lastSync": "Последняя синхронизация"
|
||||
},
|
||||
"quickAssistant": {
|
||||
"title": "Быстрый помощник",
|
||||
"click_tray_to_show": "Нажмите на иконку трея для запуска",
|
||||
"enable_quick_assistant": "Включить быстрый помощник",
|
||||
"use_shortcut_to_show": "Нажмите на иконку трея или используйте горячие клавиши для запуска"
|
||||
"webdav.user": "Пользователь WebDAV"
|
||||
},
|
||||
"display.title": "Настройки отображения",
|
||||
"font_size.title": "Размер шрифта сообщений",
|
||||
@@ -405,20 +378,9 @@
|
||||
"general.user_name.placeholder": "Введите ваше имя",
|
||||
"general.view_webdav_settings": "Просмотр настроек WebDAV",
|
||||
"general.display.title": "Настройки отображения",
|
||||
"display.sidebar.translate.icon": "Показывать иконку перевода",
|
||||
"display.sidebar.painting.icon": "Показывать иконку рисования",
|
||||
"display.sidebar.minapp.icon": "Показывать иконку мини-приложения",
|
||||
"display.sidebar.knowledge.icon": "Показывать иконку знаний",
|
||||
"display.sidebar.files.icon": "Показывать иконку файлов",
|
||||
"display.sidebar.title": "Настройки боковой панели",
|
||||
"display.sidebar.visible": "Показывать иконки",
|
||||
"display.sidebar.disabled": "Скрыть иконки",
|
||||
"display.sidebar.chat.hiddenMessage": "Помощник является базовой функцией и не поддерживает скрытие",
|
||||
"display.sidebar.empty": "Перетащите скрываемую функцию с левой стороны сюда",
|
||||
"display.minApp.title": "Настройки отображения мини программы",
|
||||
"display.minApp.visible": "Отображаемый апплет",
|
||||
"display.minApp.disabled": "скрытый апплет",
|
||||
"display.minApp.empty": "Перетащите апплет, который хотите скрыть, слева сюда",
|
||||
"display.topic.title": "Настройки топиков",
|
||||
"display.custom.css": "Пользовательский CSS",
|
||||
"display.custom.css.placeholder": "/* Здесь введите пользовательский CSS */",
|
||||
@@ -523,11 +485,7 @@
|
||||
"clear_shortcut": "Очистить сочетание клавиш",
|
||||
"toggle_show_assistants": "Переключить отображение ассистентов",
|
||||
"toggle_show_topics": "Переключить отображение топиков",
|
||||
"copy_last_message": "Копировать последнее сообщение",
|
||||
"search_message": "Поиск сообщения",
|
||||
"mini_window": "Быстрый помощник",
|
||||
"clear_topic": "Очистить все сообщения",
|
||||
"toggle_new_context": "Очистить контекст"
|
||||
"copy_last_message": "Копировать последнее сообщение"
|
||||
},
|
||||
"theme.auto": "Автоматически",
|
||||
"theme.dark": "Темная",
|
||||
@@ -560,8 +518,7 @@
|
||||
},
|
||||
"tray": {
|
||||
"quit": "Выйти",
|
||||
"show_window": "Показать окно",
|
||||
"show_mini_window": "Быстрый помощник"
|
||||
"show_window": "Показать окно"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "Граф знаний",
|
||||
@@ -569,7 +526,7 @@
|
||||
"show_window": "Показать окно",
|
||||
"quit": "Выйти"
|
||||
},
|
||||
"knowledge": {
|
||||
"knowledge_base": {
|
||||
"title": "База знаний",
|
||||
"search": "Поиск в базе знаний",
|
||||
"empty": "База знаний не найдена",
|
||||
@@ -611,7 +568,6 @@
|
||||
"directory_placeholder": "Введите путь к директории",
|
||||
"model_info": "Модель информации",
|
||||
"not_support": "База знаний базы данных движок обновлен, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
|
||||
"no_provider": "База знаний модель поставщика не настроена, база знаний больше не поддерживается, пожалуйста, создайте новую базу знаний",
|
||||
"source": "Источник"
|
||||
},
|
||||
"models": {
|
||||
@@ -638,37 +594,11 @@
|
||||
"parameter_type": {
|
||||
"string": "Текст",
|
||||
"number": "Число",
|
||||
"boolean": "Логическое",
|
||||
"json": "JSON"
|
||||
"boolean": "Логическое"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"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": "возвращения",
|
||||
"copy_last_message": "Нажмите C для копирования"
|
||||
}
|
||||
"summarize": "Вы - эксперт в общении, который суммирует разговоры пользователя в 10-символьном заголовке, совпадающем с языком пользователя, без использования знаков препинания и других специальных символов"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +64,14 @@
|
||||
"default.description": "你好,我是默认助手。你可以立刻开始跟我聊天。",
|
||||
"default.name": "⭐️ 默认助手",
|
||||
"default.topic.name": "默认话题",
|
||||
"input.clear": "清空消息 {{Command}}",
|
||||
"input.clear": "清空消息",
|
||||
"input.clear.content": "确定要清除当前会话所有消息吗?",
|
||||
"input.clear.title": "清空消息",
|
||||
"input.collapse": "收起",
|
||||
"input.context_count.tip": "上下文数",
|
||||
"input.estimated_tokens.tip": "预估 token 数",
|
||||
"input.expand": "展开",
|
||||
"input.new.context": "清除上下文 {{Command}}",
|
||||
"input.new.context": "清除上下文",
|
||||
"input.new_topic": "新话题 {{Command}}",
|
||||
"input.pause": "暂停",
|
||||
"input.placeholder": "在这里输入消息...",
|
||||
@@ -82,7 +82,7 @@
|
||||
"input.upload": "上传图片或文档",
|
||||
"input.web_search": "开启网络搜索",
|
||||
"input.knowledge_base": "知识库",
|
||||
"message.new.branch": "分支",
|
||||
"message.new.branch": "新分支",
|
||||
"message.new.branch.created": "新分支已创建",
|
||||
"message.regenerate.model": "切换模型",
|
||||
"message.new.context": "清除上下文",
|
||||
@@ -184,14 +184,8 @@
|
||||
"name": "文件名",
|
||||
"open": "打开",
|
||||
"size": "大小",
|
||||
"type": "类型",
|
||||
"text": "文本",
|
||||
"title": "文件",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"delete.title": "删除文件",
|
||||
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除此文件吗?",
|
||||
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除"
|
||||
"title": "文件"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "继续聊天",
|
||||
@@ -219,10 +213,6 @@
|
||||
"png": "下载 PNG",
|
||||
"svg": "下载 SVG"
|
||||
},
|
||||
"resize": {
|
||||
"zoom-in": "放大",
|
||||
"zoom-out": "缩小"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "预览",
|
||||
"source": "源码"
|
||||
@@ -232,7 +222,6 @@
|
||||
"message": {
|
||||
"api.connection.failed": "连接失败",
|
||||
"api.connection.success": "连接成功",
|
||||
"api.check.model.title": "请选择要检测的模型",
|
||||
"assistant.added.content": "智能体添加成功",
|
||||
"backup.failed": "备份失败",
|
||||
"backup.success": "备份成功",
|
||||
@@ -256,7 +245,7 @@
|
||||
"reset.double.confirm.title": "数据丢失!!!",
|
||||
"restore.success": "恢复成功",
|
||||
"save.success.title": "保存成功",
|
||||
"switch.disabled": "请等待当前回复完成后操作",
|
||||
"switch.disabled": "模型回复完成后才能切换",
|
||||
"topic.added": "话题添加成功",
|
||||
"upgrade.success.button": "重启",
|
||||
"upgrade.success.content": "重启用以完成升级",
|
||||
@@ -266,9 +255,7 @@
|
||||
"error.get_embedding_dimensions": "获取嵌入维度失败"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "小程序",
|
||||
"sidebar.add.title": "添加到侧边栏",
|
||||
"sidebar.remove.title": "从侧边栏移除"
|
||||
"title": "小程序"
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "对话后模型在内存中保持的时间(默认:5分钟)",
|
||||
@@ -293,9 +280,7 @@
|
||||
"regenerate.confirm": "这将覆盖已生成的图片,是否继续?",
|
||||
"seed": "随机种子",
|
||||
"seed_tip": "相同的种子和提示词可以生成相似的图片",
|
||||
"title": "图片",
|
||||
"prompt_enhancement": "提示词增强",
|
||||
"prompt_enhancement_tip": "开启后将提示重写为详细的、适合模型的版本"
|
||||
"title": "图片"
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
@@ -327,8 +312,7 @@
|
||||
"together": "Together",
|
||||
"yi": "零一万物",
|
||||
"zhinao": "360智脑",
|
||||
"zhipu": "智谱AI",
|
||||
"qwenlm": "QwenLM"
|
||||
"zhipu": "智谱AI"
|
||||
},
|
||||
"settings": {
|
||||
"about": "关于我们",
|
||||
@@ -375,22 +359,11 @@
|
||||
"webdav.password": "WebDAV 密码",
|
||||
"webdav.path": "WebDAV 路径",
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.autoSync": "自动备份",
|
||||
"webdav.autoSync": "自动同步",
|
||||
"webdav.minutes": "分钟",
|
||||
"webdav.restore.button": "从 WebDAV 恢复",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV 用户名",
|
||||
"webdav.syncStatus": "备份状态",
|
||||
"webdav.autoSync.off": "关闭",
|
||||
"webdav.noSync": "等待下次备份",
|
||||
"webdav.syncError": "备份错误",
|
||||
"webdav.lastSync": "上次备份时间"
|
||||
},
|
||||
"quickAssistant": {
|
||||
"title": "快捷助手",
|
||||
"click_tray_to_show": "点击托盘图标启动",
|
||||
"enable_quick_assistant": "启用快捷助手",
|
||||
"use_shortcut_to_show": "右键点击托盘图标或使用快捷键启动"
|
||||
"webdav.user": "WebDAV 用户名"
|
||||
},
|
||||
"display.title": "显示设置",
|
||||
"font_size.title": "消息字体大小",
|
||||
@@ -406,20 +379,9 @@
|
||||
"general.user_name.placeholder": "请输入用户名",
|
||||
"general.view_webdav_settings": "查看 WebDAV 设置",
|
||||
"general.display.title": "显示设置",
|
||||
"display.sidebar.translate.icon": "显示翻译图标",
|
||||
"display.sidebar.painting.icon": "显示绘画图标",
|
||||
"display.sidebar.minapp.icon": "显示小程序图标",
|
||||
"display.sidebar.knowledge.icon": "显示知识图标",
|
||||
"display.sidebar.files.icon": "显示文件图标",
|
||||
"display.sidebar.title": "侧边栏设置",
|
||||
"display.sidebar.visible": "显示的图标",
|
||||
"display.sidebar.disabled": "隐藏的图标",
|
||||
"display.sidebar.chat.hiddenMessage": "助手是基础功能,不支持隐藏",
|
||||
"display.sidebar.empty": "把要隐藏的功能从左侧拖拽到这里",
|
||||
"display.minApp.title": "小程序显示设置",
|
||||
"display.minApp.visible": "显示的小程序",
|
||||
"display.minApp.disabled": "隐藏的小程序",
|
||||
"display.minApp.empty": "把要隐藏的小程序从左侧拖拽到这里",
|
||||
"display.topic.title": "话题设置",
|
||||
"display.custom.css": "自定义 CSS",
|
||||
"display.custom.css.placeholder": "/* 这里写自定义CSS */",
|
||||
@@ -512,11 +474,7 @@
|
||||
"clear_shortcut": "清除快捷键",
|
||||
"toggle_show_assistants": "切换助手显示",
|
||||
"toggle_show_topics": "切换话题显示",
|
||||
"copy_last_message": "复制上一条消息",
|
||||
"search_message": "搜索消息",
|
||||
"mini_window": "快捷助手",
|
||||
"clear_topic": "清空消息",
|
||||
"toggle_new_context": "清除上下文"
|
||||
"copy_last_message": "复制上一条消息"
|
||||
},
|
||||
"theme.auto": "跟随系统",
|
||||
"theme.dark": "深色主题",
|
||||
@@ -549,8 +507,7 @@
|
||||
},
|
||||
"tray": {
|
||||
"quit": "退出",
|
||||
"show_window": "显示窗口",
|
||||
"show_mini_window": "快捷助手"
|
||||
"show_window": "显示窗口"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "知识图谱",
|
||||
@@ -558,7 +515,7 @@
|
||||
"show_window": "显示窗口",
|
||||
"quit": "退出"
|
||||
},
|
||||
"knowledge": {
|
||||
"knowledge_base": {
|
||||
"title": "知识库",
|
||||
"search": "搜索知识库",
|
||||
"empty": "暂无知识库",
|
||||
@@ -600,7 +557,6 @@
|
||||
"directory_placeholder": "请输入目录路径",
|
||||
"model_info": "模型信息",
|
||||
"not_support": "知识库数据库引擎已更新,该知识库将不再支持,请重新创建知识库",
|
||||
"no_provider": "知识库模型服务商丢失,该知识库将不再支持,请重新创建知识库",
|
||||
"source": "来源"
|
||||
},
|
||||
"models": {
|
||||
@@ -627,37 +583,11 @@
|
||||
"parameter_type": {
|
||||
"string": "文本",
|
||||
"number": "数字",
|
||||
"boolean": "布尔值",
|
||||
"json": "JSON"
|
||||
"boolean": "布尔值"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"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": "返回",
|
||||
"copy_last_message": "按 C 键复制"
|
||||
}
|
||||
"summarize": "你是一名擅长会话的助理,你需要将用户的会话总结为 10 个字以内的标题,标题语言与用户的首要语言一致,不要使用标点符号和其他特殊符号"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,14 +64,14 @@
|
||||
"default.description": "你好,我是預設助手。你可以立即開始與我聊天。",
|
||||
"default.name": "⭐️ 預設助手",
|
||||
"default.topic.name": "預設話題",
|
||||
"input.clear": "清除 {{Command}}",
|
||||
"input.clear": "清除",
|
||||
"input.clear.content": "您想要清除當前話題的所有訊息嗎?",
|
||||
"input.clear.title": "清除所有訊息?",
|
||||
"input.collapse": "收起",
|
||||
"input.context_count.tip": "上下文數量",
|
||||
"input.estimated_tokens.tip": "預估 Token 數",
|
||||
"input.expand": "展開",
|
||||
"input.new.context": "清除上下文 {{Command}}",
|
||||
"input.new.context": "清除上下文",
|
||||
"input.new_topic": "新話題 {{Command}}",
|
||||
"input.pause": "暫停",
|
||||
"input.placeholder": "在此輸入您的訊息...",
|
||||
@@ -82,7 +82,7 @@
|
||||
"input.upload": "上傳圖片或文檔",
|
||||
"input.web_search": "開啟網路搜索",
|
||||
"input.knowledge_base": "知識庫",
|
||||
"message.new.branch": "分支",
|
||||
"message.new.branch": "新分支",
|
||||
"message.new.branch.created": "新分支已建立",
|
||||
"message.regenerate.model": "切換模型",
|
||||
"message.new.context": "新上下文",
|
||||
@@ -183,14 +183,8 @@
|
||||
"name": "名稱",
|
||||
"open": "打開",
|
||||
"size": "大小",
|
||||
"type": "類型",
|
||||
"text": "文本",
|
||||
"title": "檔案",
|
||||
"edit": "編輯",
|
||||
"delete": "刪除",
|
||||
"delete.title": "刪除檔案",
|
||||
"delete.content": "刪除檔案會刪除檔案在所有消息中的引用,確定要刪除此檔案嗎?",
|
||||
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除"
|
||||
"title": "檔案"
|
||||
},
|
||||
"history": {
|
||||
"continue_chat": "繼續聊天",
|
||||
@@ -218,10 +212,6 @@
|
||||
"png": "下載 PNG",
|
||||
"svg": "下載 SVG"
|
||||
},
|
||||
"resize": {
|
||||
"zoom-in": "放大",
|
||||
"zoom-out": "縮小"
|
||||
},
|
||||
"tabs": {
|
||||
"preview": "預覽",
|
||||
"source": "原始碼"
|
||||
@@ -231,7 +221,6 @@
|
||||
"message": {
|
||||
"api.connection.failed": "連接失敗",
|
||||
"api.connection.success": "連接成功",
|
||||
"api.check.model.title": "請選擇要檢測的模型",
|
||||
"assistant.added.content": "智能體添加成功",
|
||||
"backup.failed": "備份失敗",
|
||||
"backup.success": "備份成功",
|
||||
@@ -255,7 +244,7 @@
|
||||
"reset.double.confirm.title": "資料將會丟失!!!",
|
||||
"restore.success": "恢復成功",
|
||||
"save.success.title": "保存成功",
|
||||
"switch.disabled": "請等待當前回覆完成",
|
||||
"switch.disabled": "助手生成回覆時無法切換",
|
||||
"topic.added": "新話題已添加",
|
||||
"upgrade.success.button": "重新啟動",
|
||||
"upgrade.success.content": "請重新啟動應用以完成升級",
|
||||
@@ -265,9 +254,7 @@
|
||||
"error.get_embedding_dimensions": "獲取嵌入維度失敗"
|
||||
},
|
||||
"minapp": {
|
||||
"title": "小程序",
|
||||
"sidebar.add.title": "添加到側邊欄",
|
||||
"sidebar.remove.title": "從側邊欄移除"
|
||||
"title": "小程序"
|
||||
},
|
||||
"ollama": {
|
||||
"keep_alive_time.description": "對話後模型在記憶體中保持的時間(預設為 5 分鐘)。",
|
||||
@@ -292,9 +279,7 @@
|
||||
"regenerate.confirm": "這將覆蓋已生成的圖片,是否繼續?",
|
||||
"seed": "隨機種子",
|
||||
"seed_tip": "相同的種子和提示詞可以生成相似的圖片",
|
||||
"title": "繪圖",
|
||||
"prompt_enhancement": "提示詞增強",
|
||||
"prompt_enhancement_tip": "開啟後將提示重寫為詳細的、適合模型的版本"
|
||||
"title": "繪圖"
|
||||
},
|
||||
"provider": {
|
||||
"aihubmix": "AiHubMix",
|
||||
@@ -326,8 +311,7 @@
|
||||
"together": "Together",
|
||||
"yi": "零一萬物",
|
||||
"zhinao": "360智腦",
|
||||
"zhipu": "智譜AI",
|
||||
"qwenlm": "QwenLM"
|
||||
"zhipu": "智譜AI"
|
||||
},
|
||||
"settings": {
|
||||
"about": "關於與回饋",
|
||||
@@ -374,22 +358,11 @@
|
||||
"webdav.password": "WebDAV 密碼",
|
||||
"webdav.path": "WebDAV Path",
|
||||
"webdav.path.placeholder": "/backup",
|
||||
"webdav.autoSync": "自動備份",
|
||||
"webdav.autoSync": "自動同步",
|
||||
"webdav.minutes": "分鐘",
|
||||
"webdav.restore.button": "從 WebDAV 恢復",
|
||||
"webdav.title": "WebDAV",
|
||||
"webdav.user": "WebDAV 使用者名稱",
|
||||
"webdav.syncStatus": "備份狀態",
|
||||
"webdav.autoSync.off": "關閉",
|
||||
"webdav.noSync": "等待下次備份",
|
||||
"webdav.syncError": "備份錯誤",
|
||||
"webdav.lastSync": "上次同步時間"
|
||||
},
|
||||
"quickAssistant": {
|
||||
"title": "快捷助手",
|
||||
"click_tray_to_show": "點擊托盤圖標啟動",
|
||||
"enable_quick_assistant": "啟用快捷助手",
|
||||
"use_shortcut_to_show": "右鍵點擊托盤圖標或使用快捷鍵啟動"
|
||||
"webdav.user": "WebDAV 使用者名稱"
|
||||
},
|
||||
"display.title": "顯示設定",
|
||||
"font_size.title": "訊息字體大小",
|
||||
@@ -405,21 +378,10 @@
|
||||
"general.user_name.placeholder": "輸入您的名稱",
|
||||
"general.view_webdav_settings": "查看 WebDAV 設定",
|
||||
"general.display.title": "顯示設定",
|
||||
"display.sidebar.translate.icon": "顯示翻譯圖示",
|
||||
"display.sidebar.painting.icon": "顯示繪圖圖示",
|
||||
"display.sidebar.minapp.icon": "顯示小程序圖示",
|
||||
"display.sidebar.knowledge.icon": "顯示知識圖示",
|
||||
"display.sidebar.files.icon": "顯示文件圖示",
|
||||
"display.sidebar.title": "側邊欄設定",
|
||||
"display.topic.title": "話題設定",
|
||||
"display.sidebar.chat.hiddenMessage": "助手是基礎功能,不支援隱藏",
|
||||
"display.sidebar.empty": "把要隱藏的功能從左側拖拽到這裡",
|
||||
"display.sidebar.visible": "顯示的圖標",
|
||||
"display.sidebar.disabled": "隱藏的圖標",
|
||||
"display.minApp.title": "小程序顯示設定",
|
||||
"display.minApp.visible": "顯示的小程序",
|
||||
"display.minApp.disabled": "隱藏的小程序",
|
||||
"display.minApp.empty": "把要隱藏的小程序從左側拖拽到這裡",
|
||||
"display.custom.css": "自定義 CSS",
|
||||
"display.custom.css.placeholder": "/* 這裡寫自定義 CSS */",
|
||||
"input.auto_translate_with_space": "快速敲擊3次空格翻譯",
|
||||
@@ -511,11 +473,7 @@
|
||||
"clear_shortcut": "清除快捷鍵",
|
||||
"toggle_show_assistants": "切換助手顯示",
|
||||
"toggle_show_topics": "切換話題顯示",
|
||||
"copy_last_message": "複製上一条消息",
|
||||
"search_message": "搜索消息",
|
||||
"mini_window": "快捷助手",
|
||||
"clear_topic": "清除所有訊息",
|
||||
"toggle_new_context": "清除上下文"
|
||||
"copy_last_message": "複製上一条消息"
|
||||
},
|
||||
"theme.auto": "自動",
|
||||
"theme.dark": "深色主題",
|
||||
@@ -548,8 +506,7 @@
|
||||
},
|
||||
"tray": {
|
||||
"quit": "退出",
|
||||
"show_window": "顯示視窗",
|
||||
"show_mini_window": "快捷助手"
|
||||
"show_window": "顯示視窗"
|
||||
},
|
||||
"words": {
|
||||
"knowledgeGraph": "知識圖譜",
|
||||
@@ -557,7 +514,7 @@
|
||||
"show_window": "顯示視窗",
|
||||
"quit": "退出"
|
||||
},
|
||||
"knowledge": {
|
||||
"knowledge_base": {
|
||||
"title": "知識庫",
|
||||
"search": "搜尋知識庫",
|
||||
"empty": "暫無知識庫",
|
||||
@@ -599,7 +556,6 @@
|
||||
"directory_placeholder": "請輸入目錄路徑",
|
||||
"model_info": "模型信息",
|
||||
"not_support": "知識庫數據庫引擎已更新,該知識庫將不再支持,請重新創建知識庫",
|
||||
"no_provider": "知識庫模型提供商遺失,該知識庫將不再支持,請重新創建知識庫",
|
||||
"source": "來源"
|
||||
},
|
||||
"models": {
|
||||
@@ -626,37 +582,11 @@
|
||||
"parameter_type": {
|
||||
"string": "文字",
|
||||
"number": "數字",
|
||||
"boolean": "布林值",
|
||||
"json": "JSON"
|
||||
"boolean": "布林值"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
"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": "返回",
|
||||
"copy_last_message": "按 C 鍵複製"
|
||||
}
|
||||
"summarize": "你是一名擅長會話的助理,你需要將用戶的會話總結為 10 個字以內的標題,標題語言與用戶的首要語言一致,不要使用標點符號和其他特殊符號"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,8 @@
|
||||
import KeyvStorage from '@kangfenmao/keyv-storage'
|
||||
|
||||
import { startAutoSync } from './services/BackupService'
|
||||
import store from './store'
|
||||
|
||||
function initSpinner() {
|
||||
const spinner = document.getElementById('spinner')
|
||||
if (spinner && window.location.hash !== '#/mini') {
|
||||
spinner.style.display = 'flex'
|
||||
}
|
||||
}
|
||||
|
||||
function initKeyv() {
|
||||
function init() {
|
||||
window.keyv = new KeyvStorage()
|
||||
window.keyv.init()
|
||||
}
|
||||
|
||||
function initAutoSync() {
|
||||
setTimeout(() => {
|
||||
const { webdavAutoSync } = store.getState().settings
|
||||
if (webdavAutoSync) {
|
||||
startAutoSync()
|
||||
}
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
initSpinner()
|
||||
initKeyv()
|
||||
initAutoSync()
|
||||
init()
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import './assets/styles/index.scss'
|
||||
import './init'
|
||||
|
||||
import ReactDOM from 'react-dom/client'
|
||||
|
||||
import App from './App'
|
||||
import MiniApp from './windows/mini/App'
|
||||
|
||||
if (location.hash === '#/mini') {
|
||||
document.getElementById('spinner')?.remove()
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<MiniApp />)
|
||||
} else {
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)
|
||||
}
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(<App />)
|
||||
|
||||
@@ -120,46 +120,40 @@ const AgentsPage: FC = () => {
|
||||
[i18n.language]
|
||||
)
|
||||
|
||||
const renderAgentList = useCallback(
|
||||
(agents: Agent[]) => {
|
||||
return (
|
||||
<Row gutter={[20, 20]}>
|
||||
{agents.map((agent, index) => (
|
||||
<Col span={6} key={agent.id || index}>
|
||||
<AgentCard
|
||||
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent as any))}
|
||||
agent={agent as any}
|
||||
/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
)
|
||||
},
|
||||
[onAddAgentConfirm]
|
||||
)
|
||||
|
||||
const tabItems = useMemo(() => {
|
||||
const groups = Object.keys(filteredAgentGroups)
|
||||
|
||||
return groups.map((group, i) => {
|
||||
const id = String(i + 1)
|
||||
const localizedGroupName = getLocalizedGroupName(group)
|
||||
const agents = filteredAgentGroups[group] || []
|
||||
|
||||
return {
|
||||
label: localizedGroupName,
|
||||
key: id,
|
||||
children: (
|
||||
<TabContent key={group}>
|
||||
<Title level={5} key={group} style={{ marginBottom: 10 }}>
|
||||
<Title level={5} key={group} style={{ marginBottom: 16 }}>
|
||||
{localizedGroupName}
|
||||
</Title>
|
||||
{group === '我的' ? <MyAgents onClick={onAddAgentConfirm} search={search} /> : renderAgentList(agents)}
|
||||
<Row gutter={[20, 20]}>
|
||||
{group === '我的' ? (
|
||||
<MyAgents onClick={onAddAgentConfirm} search={search} />
|
||||
) : (
|
||||
filteredAgentGroups[group]?.map((agent, index) => (
|
||||
<Col span={6} key={group + index}>
|
||||
<AgentCard
|
||||
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent as any))}
|
||||
agent={agent as any}
|
||||
/>
|
||||
</Col>
|
||||
))
|
||||
)}
|
||||
</Row>
|
||||
</TabContent>
|
||||
)
|
||||
}
|
||||
})
|
||||
}, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm, search, renderAgentList])
|
||||
}, [filteredAgentGroups, getLocalizedGroupName, onAddAgentConfirm, search])
|
||||
|
||||
const handleSearch = () => {
|
||||
if (searchInput.trim() === '') {
|
||||
@@ -195,9 +189,22 @@ const AgentsPage: FC = () => {
|
||||
<AssistantsContainer>
|
||||
{Object.values(filteredAgentGroups).flat().length > 0 ? (
|
||||
search.trim() ? (
|
||||
<TabContent>{renderAgentList(Object.values(filteredAgentGroups).flat())}</TabContent>
|
||||
<TabContent>
|
||||
<Row gutter={[20, 20]}>
|
||||
{Object.values(filteredAgentGroups)
|
||||
.flat()
|
||||
.map((agent, index, array) => (
|
||||
<Col span={array.length === 1 ? 12 : 6} key={index}>
|
||||
<AgentCard
|
||||
onClick={() => onAddAgentConfirm(getAgentFromSystemAgent(agent as any))}
|
||||
agent={agent as any}
|
||||
/>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</TabContent>
|
||||
) : (
|
||||
<Tabs tabPosition="right" animated={false} items={tabItems} $language={i18n.language} />
|
||||
<Tabs tabPosition="right" animated items={tabItems} $language={i18n.language} />
|
||||
)
|
||||
) : (
|
||||
<EmptyView>
|
||||
@@ -225,7 +232,6 @@ const ContentContainer = styled.div`
|
||||
height: 100%;
|
||||
padding: 0 10px;
|
||||
padding-left: 0;
|
||||
border-top: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
const AssistantsContainer = styled.div`
|
||||
@@ -241,9 +247,6 @@ const TabContent = styled(Scrollbar)`
|
||||
margin-right: -4px;
|
||||
padding-bottom: 20px !important;
|
||||
overflow-x: hidden;
|
||||
transform: translateZ(0);
|
||||
will-change: transform;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
`
|
||||
|
||||
const AgentPrompt = styled.div`
|
||||
@@ -265,15 +268,12 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.ant-tabs-tabpane {
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
.ant-tabs-nav {
|
||||
min-width: ${({ $language }) => ($language.startsWith('zh') ? '120px' : '140px')};
|
||||
max-width: ${({ $language }) => ($language.startsWith('zh') ? '120px' : '140px')};
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')};
|
||||
max-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')};
|
||||
}
|
||||
.ant-tabs-nav-list {
|
||||
padding: 10px 8px;
|
||||
@@ -283,7 +283,7 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
|
||||
}
|
||||
.ant-tabs-tab {
|
||||
margin: 0 !important;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 16px;
|
||||
margin-bottom: 5px !important;
|
||||
font-size: 13px;
|
||||
justify-content: left;
|
||||
@@ -291,14 +291,11 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
|
||||
border: 0.5px solid transparent;
|
||||
justify-content: ${({ $language }) => ($language.startsWith('zh') ? 'center' : 'flex-start')};
|
||||
user-select: none;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
|
||||
.ant-tabs-tab-btn {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 100px;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--color-text) !important;
|
||||
@@ -307,8 +304,8 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
|
||||
}
|
||||
.ant-tabs-tab-active {
|
||||
background-color: var(--color-background-soft);
|
||||
border-right: none;
|
||||
border: 0.5px solid var(--color-border);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
.ant-tabs-content-holder {
|
||||
border-left: 0.5px solid var(--color-border);
|
||||
@@ -325,9 +322,6 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
|
||||
color: var(--color-text) !important;
|
||||
}
|
||||
}
|
||||
.ant-tabs-content {
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
}
|
||||
`
|
||||
|
||||
export default AgentsPage
|
||||
|
||||
@@ -2,12 +2,11 @@ import { EllipsisOutlined } from '@ant-design/icons'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { getLeadingEmoji } from '@renderer/utils'
|
||||
import { Dropdown } from 'antd'
|
||||
import { FC, memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
agent: Agent
|
||||
onClick: () => void
|
||||
onClick?: () => void
|
||||
contextMenu?: { label: string; onClick: () => void }[]
|
||||
menuItems?: {
|
||||
key: string
|
||||
@@ -18,7 +17,7 @@ interface Props {
|
||||
}[]
|
||||
}
|
||||
|
||||
const AgentCard: FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => {
|
||||
const AgentCard: React.FC<Props> = ({ agent, onClick, contextMenu, menuItems }) => {
|
||||
const emoji = agent.emoji || getLeadingEmoji(agent.name)
|
||||
const prompt = (agent.description || agent.prompt).substring(0, 100).replace(/\\n/g, '')
|
||||
const content = (
|
||||
@@ -82,7 +81,7 @@ const Container = styled.div`
|
||||
text-align: center;
|
||||
gap: 10px;
|
||||
background-color: var(--color-background);
|
||||
border-radius: 10px;
|
||||
border-radius: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
@@ -206,4 +205,4 @@ const MenuContainer = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
export default memo(AgentCard)
|
||||
export default AgentCard
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||
import { Agent } from '@renderer/types'
|
||||
import { Col, Row } from 'antd'
|
||||
import { Col } from 'antd'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -43,7 +43,7 @@ const MyAgents: React.FC<Props> = ({ onClick, search }) => {
|
||||
)
|
||||
|
||||
return (
|
||||
<Row gutter={[20, 20]}>
|
||||
<>
|
||||
{filteredAgents.map((agent) => {
|
||||
const dropdownMenuItems = [
|
||||
{
|
||||
@@ -102,7 +102,7 @@ const MyAgents: React.FC<Props> = ({ onClick, search }) => {
|
||||
<Col span={6}>
|
||||
<AddAgentCard onClick={() => AddAgentPopup.show()} />
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { MinAppType } from '@renderer/types'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Dropdown } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
@@ -15,37 +10,23 @@ interface Props {
|
||||
}
|
||||
|
||||
const App: FC<Props> = ({ app, onClick, size = 60 }) => {
|
||||
const { t } = useTranslation()
|
||||
const { minapps, pinned, updatePinnedMinapps } = useMinapps()
|
||||
const isPinned = pinned.some((p) => p.id === app.id)
|
||||
const isVisible = minapps.some((m) => m.id === app.id)
|
||||
|
||||
const handleClick = () => {
|
||||
MinApp.start(app)
|
||||
onClick?.()
|
||||
}
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
{
|
||||
key: 'togglePin',
|
||||
label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.add.title'),
|
||||
onClick: () => {
|
||||
console.debug('togglePin', app)
|
||||
const newPinned = isPinned ? pinned.filter((item) => item.id !== app.id) : [...(pinned || []), app]
|
||||
updatePinnedMinapps(newPinned)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
if (!isVisible) return null
|
||||
|
||||
return (
|
||||
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
|
||||
<Container onClick={handleClick}>
|
||||
<MinAppIcon size={size} app={app} />
|
||||
<AppTitle>{app.name}</AppTitle>
|
||||
</Container>
|
||||
</Dropdown>
|
||||
<Container onClick={handleClick}>
|
||||
<AppIcon
|
||||
src={app.logo}
|
||||
style={{
|
||||
border: app.bodered ? '0.5px solid var(--color-border)' : 'none',
|
||||
width: `${size}px`,
|
||||
height: `${size}px`
|
||||
}}
|
||||
/>
|
||||
<AppTitle>{app.name}</AppTitle>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -58,6 +39,12 @@ const Container = styled.div`
|
||||
overflow: hidden;
|
||||
`
|
||||
|
||||
const AppIcon = styled.img`
|
||||
border-radius: 16px;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
`
|
||||
|
||||
const AppTitle = styled.div`
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { SearchOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import { Center } from '@renderer/components/Layout'
|
||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||
import { getAllMinApps } from '@renderer/config/minapps'
|
||||
import { Empty, Input } from 'antd'
|
||||
import { isEmpty } from 'lodash'
|
||||
import React, { FC, useState } from 'react'
|
||||
import { FC, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -13,27 +13,16 @@ import App from './App'
|
||||
const AppsPage: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [search, setSearch] = useState('')
|
||||
const { minapps } = useMinapps()
|
||||
const apps = useMemo(() => getAllMinApps(), [])
|
||||
|
||||
const filteredApps = search
|
||||
? minapps.filter(
|
||||
? apps.filter(
|
||||
(app) => app.name.toLowerCase().includes(search.toLowerCase()) || app.url.includes(search.toLowerCase())
|
||||
)
|
||||
: minapps
|
||||
|
||||
// Calculate the required number of lines
|
||||
const itemsPerRow = Math.floor(930 / 115) // Maximum width divided by the width of each item (including spacing)
|
||||
const rowCount = Math.ceil(filteredApps.length / itemsPerRow)
|
||||
// Each line height is 85px (60px icon + 5px margin + 12px text + spacing)
|
||||
const containerHeight = rowCount * 85 + (rowCount - 1) * 25 // 25px is the line spacing.
|
||||
|
||||
// Disable right-click menu in blank area
|
||||
const handleContextMenu = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
: apps
|
||||
|
||||
return (
|
||||
<Container onContextMenu={handleContextMenu}>
|
||||
<Container>
|
||||
<Navbar>
|
||||
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
||||
{t('minapp.title')}
|
||||
@@ -51,7 +40,7 @@ const AppsPage: FC = () => {
|
||||
</NavbarCenter>
|
||||
</Navbar>
|
||||
<ContentContainer id="content-container">
|
||||
<AppsContainer style={{ height: containerHeight }}>
|
||||
<AppsContainer>
|
||||
{filteredApps.map((app) => (
|
||||
<App key={app.id} app={app} />
|
||||
))}
|
||||
@@ -79,18 +68,18 @@ const ContentContainer = styled.div`
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-y: scroll;
|
||||
padding: 50px;
|
||||
`
|
||||
|
||||
const AppsContainer = styled.div`
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
min-width: 930px;
|
||||
max-width: 930px;
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(auto-fill, 90px);
|
||||
gap: 25px;
|
||||
justify-content: center;
|
||||
max-height: 500px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, minmax(90px, 1fr));
|
||||
gap: 25px 25px;
|
||||
`
|
||||
|
||||
export default AppsPage
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import { FileType, FileTypes } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import { Col, Image, Row, Spin, Table } from 'antd'
|
||||
import React, { memo } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import GeminiFiles from './GeminiFiles'
|
||||
|
||||
interface ContentViewProps {
|
||||
id: FileTypes | 'all' | string
|
||||
files?: FileType[]
|
||||
dataSource?: any[]
|
||||
columns: any[]
|
||||
}
|
||||
|
||||
const ContentView: React.FC<ContentViewProps> = ({ id, files, dataSource, columns }) => {
|
||||
if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
|
||||
return (
|
||||
<Image.PreviewGroup>
|
||||
<Row gutter={[16, 16]}>
|
||||
{files?.map((file) => (
|
||||
<Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
|
||||
<ImageWrapper>
|
||||
<LoadingWrapper>
|
||||
<Spin />
|
||||
</LoadingWrapper>
|
||||
<Image
|
||||
src={FileManager.getFileUrl(file)}
|
||||
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
|
||||
preview={{ mask: false }}
|
||||
onLoad={(e) => {
|
||||
const img = e.target as HTMLImageElement
|
||||
img.parentElement?.classList.add('loaded')
|
||||
}}
|
||||
/>
|
||||
<ImageInfo>
|
||||
<div>{formatFileSize(file)}</div>
|
||||
</ImageInfo>
|
||||
</ImageWrapper>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Image.PreviewGroup>
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith('gemini_')) {
|
||||
return <GeminiFiles id={id.replace('gemini_', '') as string} />
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
dataSource={dataSource}
|
||||
columns={columns}
|
||||
style={{ width: '100%' }}
|
||||
size="small"
|
||||
pagination={{ pageSize: 100 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ImageWrapper = styled.div`
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-background-soft);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0.5px solid var(--color-border);
|
||||
|
||||
.ant-image {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
|
||||
&.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ant-image.loaded {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
div:last-child {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const LoadingWrapper = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color-background-soft);
|
||||
`
|
||||
|
||||
const ImageInfo = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
padding: 5px 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
font-size: 12px;
|
||||
|
||||
> div:first-child {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`
|
||||
|
||||
export default memo(ContentView)
|
||||
@@ -1,36 +1,20 @@
|
||||
import {
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
EllipsisOutlined,
|
||||
FileImageOutlined,
|
||||
FilePdfOutlined,
|
||||
FileTextOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { FileImageOutlined, FilePdfOutlined, FileTextOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import db from '@renderer/databases'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import FileManager from '@renderer/services/FileManager'
|
||||
import store from '@renderer/store'
|
||||
import { FileType, FileTypes } from '@renderer/types'
|
||||
import { formatFileSize } from '@renderer/utils'
|
||||
import type { MenuProps } from 'antd'
|
||||
import { Button, Dropdown, Menu } from 'antd'
|
||||
import { Col, Image, Menu, Row, Spin, Table } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { FC, useMemo, useState } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ContentView from './ContentView'
|
||||
|
||||
const FilesPage: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [fileType, setFileType] = useState<FileTypes | 'all' | 'gemini'>('all')
|
||||
const { providers } = useProviders()
|
||||
|
||||
const geminiProviders = providers.filter((provider) => provider.type === 'gemini')
|
||||
const [fileType, setFileType] = useState<FileTypes | 'all'>('all')
|
||||
|
||||
const files = useLiveQuery<FileType[]>(() => {
|
||||
if (fileType === 'all') {
|
||||
@@ -39,146 +23,56 @@ const FilesPage: FC = () => {
|
||||
return db.files.where('type').equals(fileType).sortBy('count')
|
||||
}, [fileType])
|
||||
|
||||
const handleDelete = async (fileId: string) => {
|
||||
const file = await FileManager.getFile(fileId)
|
||||
|
||||
const paintings = await store.getState().paintings.paintings
|
||||
const paintingsFiles = paintings.flatMap((p) => p.files)
|
||||
|
||||
if (paintingsFiles.some((p) => p.id === fileId)) {
|
||||
window.modal.warning({ content: t('files.delete.paintings.warning'), centered: true })
|
||||
return
|
||||
}
|
||||
|
||||
if (file) {
|
||||
await FileManager.deleteFile(fileId, true)
|
||||
}
|
||||
|
||||
const topics = await db.topics
|
||||
.filter((topic) => topic.messages.some((message) => message.files?.some((f) => f.id === fileId)))
|
||||
.toArray()
|
||||
|
||||
if (topics.length > 0) {
|
||||
for (const topic of topics) {
|
||||
const updatedMessages = topic.messages.map((message) => ({
|
||||
...message,
|
||||
files: message.files?.filter((f) => f.id !== fileId)
|
||||
}))
|
||||
await db.topics.update(topic.id, { messages: updatedMessages })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRename = async (fileId: string) => {
|
||||
const file = await FileManager.getFile(fileId)
|
||||
if (file) {
|
||||
const newName = await TextEditPopup.show({ text: file.origin_name })
|
||||
if (newName) {
|
||||
FileManager.updateFile({ ...file, origin_name: newName })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getActionMenu = (fileId: string): MenuProps['items'] => [
|
||||
{
|
||||
key: 'rename',
|
||||
icon: <EditOutlined />,
|
||||
label: t('files.edit'),
|
||||
onClick: () => handleRename(fileId)
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: <DeleteOutlined />,
|
||||
label: t('files.delete'),
|
||||
danger: true,
|
||||
onClick: () => {
|
||||
window.modal.confirm({
|
||||
title: t('files.delete.title'),
|
||||
content: t('files.delete.content'),
|
||||
centered: true,
|
||||
okButtonProps: { danger: true },
|
||||
onOk: () => handleDelete(fileId)
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const dataSource = files?.map((file) => {
|
||||
return {
|
||||
key: file.id,
|
||||
file: (
|
||||
<FileNameText className="text-nowrap" onClick={() => window.api.file.openPath(file.path)}>
|
||||
{file.origin_name}
|
||||
</FileNameText>
|
||||
),
|
||||
file: <FileNameText className="text-nowrap">{file.origin_name}</FileNameText>,
|
||||
size: formatFileSize(file),
|
||||
size_bytes: file.size,
|
||||
count: file.count,
|
||||
created_at: dayjs(file.created_at).format('MM-DD HH:mm'),
|
||||
created_at_unix: dayjs(file.created_at).unix(),
|
||||
actions: (
|
||||
<Dropdown menu={{ items: getActionMenu(file.id) }} trigger={['click']} placement="bottom" arrow>
|
||||
<Button type="text" size="small" icon={<EllipsisOutlined />} />
|
||||
</Dropdown>
|
||||
)
|
||||
actions: <a href={'file://' + FileManager.getSafePath(file)}>{t('files.open')}</a>
|
||||
}
|
||||
})
|
||||
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
title: t('files.name'),
|
||||
dataIndex: 'file',
|
||||
key: 'file',
|
||||
width: '300px'
|
||||
},
|
||||
{
|
||||
title: t('files.size'),
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
width: '80px',
|
||||
sorter: (a: { size_bytes: number }, b: { size_bytes: number }) => b.size_bytes - a.size_bytes,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: t('files.count'),
|
||||
dataIndex: 'count',
|
||||
key: 'count',
|
||||
width: '60px',
|
||||
sorter: (a: { count: number }, b: { count: number }) => b.count - a.count,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: t('files.created_at'),
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: '120px',
|
||||
align: 'center',
|
||||
sorter: (a: { created_at_unix: number }, b: { created_at_unix: number }) =>
|
||||
b.created_at_unix - a.created_at_unix
|
||||
},
|
||||
{
|
||||
title: t('files.actions'),
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
width: '80px',
|
||||
align: 'center'
|
||||
}
|
||||
],
|
||||
[t]
|
||||
)
|
||||
const columns = [
|
||||
{
|
||||
title: t('files.name'),
|
||||
dataIndex: 'file',
|
||||
key: 'file',
|
||||
width: '300px'
|
||||
},
|
||||
{
|
||||
title: t('files.size'),
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
width: '80px'
|
||||
},
|
||||
{
|
||||
title: t('files.count'),
|
||||
dataIndex: 'count',
|
||||
key: 'count',
|
||||
width: '60px'
|
||||
},
|
||||
{
|
||||
title: t('files.created_at'),
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: '120px'
|
||||
},
|
||||
{
|
||||
title: t('files.actions'),
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
width: '50px'
|
||||
}
|
||||
]
|
||||
|
||||
const menuItems = [
|
||||
{ key: 'all', label: t('files.all'), icon: <FileTextOutlined /> },
|
||||
{ key: FileTypes.IMAGE, label: t('files.image'), icon: <FileImageOutlined /> },
|
||||
{ key: FileTypes.TEXT, label: t('files.text'), icon: <FileTextOutlined /> },
|
||||
{ key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FilePdfOutlined /> },
|
||||
...geminiProviders.map((provider) => ({
|
||||
key: 'gemini_' + provider.id,
|
||||
label: provider.name,
|
||||
icon: <FilePdfOutlined />
|
||||
}))
|
||||
].filter(Boolean) as MenuProps['items']
|
||||
{ key: FileTypes.DOCUMENT, label: t('files.document'), icon: <FilePdfOutlined /> }
|
||||
]
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -190,7 +84,41 @@ const FilesPage: FC = () => {
|
||||
<Menu selectedKeys={[fileType]} items={menuItems} onSelect={({ key }) => setFileType(key as FileTypes)} />
|
||||
</SideNav>
|
||||
<TableContainer right>
|
||||
<ContentView id={fileType} files={files} dataSource={dataSource} columns={columns} />
|
||||
{fileType === FileTypes.IMAGE && files?.length && files?.length > 0 ? (
|
||||
<Image.PreviewGroup>
|
||||
<Row gutter={[16, 16]}>
|
||||
{files?.map((file) => (
|
||||
<Col key={file.id} xs={24} sm={12} md={8} lg={4} xl={3}>
|
||||
<ImageWrapper>
|
||||
<LoadingWrapper>
|
||||
<Spin />
|
||||
</LoadingWrapper>
|
||||
<Image
|
||||
src={FileManager.getFileUrl(file)}
|
||||
style={{ height: '100%', objectFit: 'cover', cursor: 'pointer' }}
|
||||
preview={{ mask: false }}
|
||||
onLoad={(e) => {
|
||||
const img = e.target as HTMLImageElement
|
||||
img.parentElement?.classList.add('loaded')
|
||||
}}
|
||||
/>
|
||||
<ImageInfo>
|
||||
<div>{formatFileSize(file)}</div>
|
||||
</ImageInfo>
|
||||
</ImageWrapper>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Image.PreviewGroup>
|
||||
) : (
|
||||
<Table
|
||||
dataSource={dataSource}
|
||||
columns={columns}
|
||||
style={{ width: '100%' }}
|
||||
size="small"
|
||||
pagination={{ pageSize: 100 }}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
@@ -221,7 +149,72 @@ const TableContainer = styled(Scrollbar)`
|
||||
const FileNameText = styled.div`
|
||||
font-size: 14px;
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const ImageWrapper = styled.div`
|
||||
position: relative;
|
||||
aspect-ratio: 1;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background-color: var(--color-background-soft);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0.5px solid var(--color-border);
|
||||
|
||||
.ant-image {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
transition:
|
||||
opacity 0.3s ease,
|
||||
transform 0.3s ease;
|
||||
|
||||
&.loaded {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.ant-image.loaded {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
div:last-child {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const LoadingWrapper = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color-background-soft);
|
||||
`
|
||||
|
||||
const ImageInfo = styled.div`
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
color: white;
|
||||
padding: 5px 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
font-size: 12px;
|
||||
|
||||
> div:first-child {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
`
|
||||
|
||||
const SideNav = styled.div`
|
||||
@@ -240,7 +233,7 @@ const SideNav = styled.div`
|
||||
line-height: 36px;
|
||||
margin: 4px 0;
|
||||
width: 100%;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 16px;
|
||||
border: 0.5px solid transparent;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { DeleteOutlined } from '@ant-design/icons'
|
||||
import type { FileMetadataResponse } from '@google/generative-ai/server'
|
||||
import { useProvider } from '@renderer/hooks/useProvider'
|
||||
import { runAsyncFunction } from '@renderer/utils'
|
||||
import { Table } from 'antd'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import { FC, useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface GeminiFilesProps {
|
||||
id: string
|
||||
}
|
||||
|
||||
const GeminiFiles: FC<GeminiFilesProps> = ({ id }) => {
|
||||
const { provider } = useProvider(id)
|
||||
const [files, setFiles] = useState<FileMetadataResponse[]>([])
|
||||
const { t } = useTranslation()
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const fetchFiles = useCallback(async () => {
|
||||
const { files } = await window.api.gemini.listFiles(provider.apiKey)
|
||||
files && setFiles(files.filter((file) => file.state === 'ACTIVE'))
|
||||
}, [provider])
|
||||
|
||||
const columns: ColumnsType<FileMetadataResponse> = [
|
||||
{
|
||||
title: t('files.name'),
|
||||
dataIndex: 'displayName',
|
||||
key: 'displayName'
|
||||
},
|
||||
{
|
||||
title: t('files.type'),
|
||||
dataIndex: 'mimeType',
|
||||
key: 'mimeType'
|
||||
},
|
||||
{
|
||||
title: t('files.size'),
|
||||
dataIndex: 'sizeBytes',
|
||||
key: 'sizeBytes',
|
||||
render: (size: string) => `${(parseInt(size) / 1024 / 1024).toFixed(2)} MB`
|
||||
},
|
||||
{
|
||||
title: t('files.created_at'),
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
render: (time: string) => new Date(time).toLocaleString()
|
||||
},
|
||||
{
|
||||
title: t('files.actions'),
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
align: 'center',
|
||||
render: (_, record) => {
|
||||
return (
|
||||
<DeleteOutlined
|
||||
style={{ cursor: 'pointer', color: 'var(--color-error)' }}
|
||||
onClick={() => {
|
||||
setFiles(files.filter((file) => file.name !== record.name))
|
||||
window.api.gemini.deleteFile(provider.apiKey, record.name).catch((error) => {
|
||||
console.error('Failed to delete file:', error)
|
||||
setFiles((prev) => [...prev, record])
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
runAsyncFunction(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
await fetchFiles()
|
||||
setLoading(false)
|
||||
} catch (error: any) {
|
||||
console.error('Failed to fetch files:', error)
|
||||
window.message.error(error.message)
|
||||
setLoading(false)
|
||||
}
|
||||
})
|
||||
}, [fetchFiles])
|
||||
|
||||
useEffect(() => {
|
||||
setFiles([])
|
||||
}, [id])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Table columns={columns} dataSource={files} rowKey="name" loading={loading} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div``
|
||||
|
||||
export default GeminiFiles
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ArrowLeftOutlined, EnterOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { Message, Topic } from '@renderer/types'
|
||||
import { Input, InputRef } from 'antd'
|
||||
import { Input } from 'antd'
|
||||
import { last } from 'lodash'
|
||||
import { FC, useEffect, useRef, useState } from 'react'
|
||||
import { FC, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
@@ -25,7 +25,6 @@ const TopicsPage: FC = () => {
|
||||
const [stack, setStack] = useState<Route[]>(_stack)
|
||||
const [topic, setTopic] = useState<Topic | undefined>(_topic)
|
||||
const [message, setMessage] = useState<Message | undefined>(_message)
|
||||
const inputRef = useRef<InputRef>(null)
|
||||
|
||||
_search = search
|
||||
_stack = stack
|
||||
@@ -59,12 +58,6 @@ const TopicsPage: FC = () => {
|
||||
|
||||
const isShow = (route: Route) => (last(stack) === route ? 'flex' : 'none')
|
||||
|
||||
useEffect(() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus()
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Header>
|
||||
@@ -79,9 +72,7 @@ const TopicsPage: FC = () => {
|
||||
placeholder={t('history.search.placeholder')}
|
||||
type="search"
|
||||
value={search}
|
||||
autoFocus
|
||||
allowClear
|
||||
ref={inputRef}
|
||||
onChange={(e) => setSearch(e.target.value.trimStart())}
|
||||
suffix={search.length >= 2 ? <EnterOutlined /> : <SearchOutlined />}
|
||||
onPressEnter={onSearch}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
||||
import NavigationService from '@renderer/services/NavigationService'
|
||||
import { Assistant } from '@renderer/types'
|
||||
@@ -22,7 +22,7 @@ const HomePage: FC = () => {
|
||||
|
||||
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
|
||||
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
|
||||
const { showAssistants, showTopics, topicPosition } = useSettings()
|
||||
const { showAssistants } = useShowAssistants()
|
||||
|
||||
_activeAssistant = activeAssistant
|
||||
|
||||
@@ -35,17 +35,8 @@ const HomePage: FC = () => {
|
||||
state?.topic && setActiveTopic(state?.topic)
|
||||
}, [state])
|
||||
|
||||
useEffect(() => {
|
||||
const canMinimize = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics
|
||||
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
|
||||
|
||||
return () => {
|
||||
window.api.window.resetMinimumSize()
|
||||
}
|
||||
}, [showAssistants, showTopics, topicPosition])
|
||||
|
||||
return (
|
||||
<Container id="home-page">
|
||||
<Container>
|
||||
<Navbar activeAssistant={activeAssistant} activeTopic={activeTopic} setActiveTopic={setActiveTopic} />
|
||||
<ContentContainer id="content-container">
|
||||
{showAssistants && (
|
||||
|
||||
@@ -16,9 +16,9 @@ const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ContentContainer>
|
||||
<Container>
|
||||
<Upload
|
||||
listType={files.length > 20 ? 'text' : 'picture-card'}
|
||||
listType="picture-card"
|
||||
fileList={files.map((file) => ({
|
||||
uid: file.id,
|
||||
url: 'file://' + FileManager.getSafePath(file),
|
||||
@@ -27,15 +27,17 @@ const AttachmentPreview: FC<Props> = ({ files, setFiles }) => {
|
||||
}))}
|
||||
onRemove={(item) => setFiles(files.filter((file) => item.uid !== file.id))}
|
||||
/>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
max-height: 40vh;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
padding: 10px 15px 0;
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
padding: 10px 20px;
|
||||
background: var(--color-background);
|
||||
border-top: 1px solid var(--color-border-mute);
|
||||
`
|
||||
|
||||
export default AttachmentPreview
|
||||
|
||||
@@ -13,7 +13,7 @@ import TranslateButton from '@renderer/components/TranslateButton'
|
||||
import { isVisionModel, isWebSearchModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
||||
@@ -24,8 +24,8 @@ import { estimateTextTokens as estimateTxtTokens } from '@renderer/services/Toke
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import store, { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||
import { setGenerating, setSearching } from '@renderer/store/runtime'
|
||||
import { Assistant, FileType, KnowledgeBase, Message, Model, Topic } from '@renderer/types'
|
||||
import { classNames, delay, getFileExtension, uuid } from '@renderer/utils'
|
||||
import { Assistant, FileType, KnowledgeBase, Message, Topic } 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'
|
||||
@@ -35,12 +35,9 @@ import { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState }
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import NarrowLayout from '../Messages/NarrowLayout'
|
||||
import AttachmentButton from './AttachmentButton'
|
||||
import AttachmentPreview from './AttachmentPreview'
|
||||
import KnowledgeBaseButton from './KnowledgeBaseButton'
|
||||
import MentionModelsButton from './MentionModelsButton'
|
||||
import MentionModelsInput from './MentionModelsInput'
|
||||
import SendMessageButton from './SendMessageButton'
|
||||
import TokenCount from './TokenCount'
|
||||
|
||||
@@ -65,8 +62,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
showInputEstimatedTokens,
|
||||
clickAssistantToShowTopic,
|
||||
language,
|
||||
autoTranslateWithSpace,
|
||||
sidebarIcons
|
||||
autoTranslateWithSpace
|
||||
} = useSettings()
|
||||
const [expended, setExpend] = useState(false)
|
||||
const [estimateTokenCount, setEstimateTokenCount] = useState(0)
|
||||
@@ -84,21 +80,16 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
const spaceClickTimer = useRef<NodeJS.Timeout>()
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [selectedKnowledgeBase, setSelectedKnowledgeBase] = useState<KnowledgeBase | undefined>(_base)
|
||||
const [mentionModels, setMentionModels] = useState<Model[]>([])
|
||||
|
||||
const isVision = useMemo(() => isVisionModel(model), [model])
|
||||
const supportExts = useMemo(() => [...textExts, ...documentExts, ...(isVision ? imageExts : [])], [isVision])
|
||||
|
||||
const showKnowledgeIcon = sidebarIcons.visible.includes('knowledge')
|
||||
|
||||
const estimateTextTokens = useCallback(debounce(estimateTxtTokens, 1000), [])
|
||||
const inputTokenCount = useMemo(
|
||||
() => (showInputEstimatedTokens ? estimateTextTokens(text) || 0 : 0),
|
||||
[estimateTextTokens, showInputEstimatedTokens, text]
|
||||
)
|
||||
const newTopicShortcut = useShortcutDisplay('new_topic')
|
||||
const newContextShortcut = useShortcutDisplay('toggle_new_context')
|
||||
const cleanTopicShortcut = useShortcutDisplay('clear_topic')
|
||||
const inputEmpty = isEmpty(text.trim()) && files.length === 0
|
||||
|
||||
_text = text
|
||||
@@ -106,7 +97,9 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
_base = selectedKnowledgeBase
|
||||
|
||||
const sendMessage = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
if (generating) {
|
||||
return
|
||||
}
|
||||
|
||||
if (inputEmpty) {
|
||||
return
|
||||
@@ -131,20 +124,15 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
message.files = await FileManager.uploadFiles(files)
|
||||
}
|
||||
|
||||
if (mentionModels.length > 0) {
|
||||
message.mentions = mentionModels
|
||||
}
|
||||
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
|
||||
|
||||
setText('')
|
||||
setFiles([])
|
||||
setMentionModels([])
|
||||
setTimeout(() => setText(''), 500)
|
||||
setTimeout(() => resizeTextArea(), 0)
|
||||
|
||||
setExpend(false)
|
||||
}, [inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files, mentionModels])
|
||||
}, [generating, inputEmpty, text, assistant.id, assistant.topics, selectedKnowledgeBase, files])
|
||||
|
||||
const translate = async () => {
|
||||
if (isTranslating) {
|
||||
@@ -190,7 +178,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
|
||||
if (expended) {
|
||||
if (event.key === 'Escape') {
|
||||
return onToggleExpended()
|
||||
return setExpend(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +207,10 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
}
|
||||
|
||||
const addNewTopic = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
if (generating) {
|
||||
window.message.warning({ content: t('message.switch.disabled'), key: 'generating' })
|
||||
return
|
||||
}
|
||||
|
||||
const topic = getDefaultTopic(assistant.id)
|
||||
|
||||
@@ -235,7 +226,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
setActiveTopic(topic)
|
||||
|
||||
clickAssistantToShowTopic && setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 0)
|
||||
}, [addTopic, assistant, clickAssistantToShowTopic, setActiveTopic, setModel])
|
||||
}, [addTopic, assistant, clickAssistantToShowTopic, generating, setActiveTopic, setModel, t])
|
||||
|
||||
const clearTopic = async () => {
|
||||
if (generating) {
|
||||
@@ -283,31 +274,25 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
|
||||
const onPaste = useCallback(
|
||||
async (event: ClipboardEvent) => {
|
||||
const clipboardText = event.clipboardData?.getData('text')
|
||||
if (clipboardText) {
|
||||
// Prioritize the text when pasting.
|
||||
// handled by the default event
|
||||
} else {
|
||||
for (const file of event.clipboardData?.files || []) {
|
||||
event.preventDefault()
|
||||
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 (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 (file.path) {
|
||||
if (supportExts.includes(getFileExtension(file.path))) {
|
||||
const selectedFile = await window.api.file.get(file.path)
|
||||
selectedFile && setFiles((prevFiles) => [...prevFiles, selectedFile])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,14 +348,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
}
|
||||
})
|
||||
|
||||
useShortcut('clear_topic', () => {
|
||||
clearTopic()
|
||||
})
|
||||
|
||||
useShortcut('toggle_new_context', () => {
|
||||
onNewContext()
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
|
||||
const unsubscribes = [
|
||||
@@ -404,152 +381,122 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('focus', () => {
|
||||
textareaRef.current?.focus()
|
||||
})
|
||||
}, [])
|
||||
|
||||
const textareaRows = window.innerHeight >= 1000 || isBubbleStyle ? 2 : 1
|
||||
|
||||
const handleKnowledgeBaseSelect = (base?: KnowledgeBase) => {
|
||||
setSelectedKnowledgeBase(base)
|
||||
}
|
||||
|
||||
const onMentionModel = useCallback(
|
||||
(model: Model) => {
|
||||
const isSelected = mentionModels.some((m) => m.id === model.id)
|
||||
if (isSelected) {
|
||||
setMentionModels(mentionModels.filter((m) => m.id !== model.id))
|
||||
} else {
|
||||
setMentionModels([...mentionModels, model])
|
||||
}
|
||||
},
|
||||
[mentionModels]
|
||||
)
|
||||
|
||||
const handleRemoveModel = (model: Model) => {
|
||||
setMentionModels(mentionModels.filter((m) => m.id !== model.id))
|
||||
}
|
||||
|
||||
return (
|
||||
<Container onDragOver={handleDragOver} onDrop={handleDrop} className="inputbar">
|
||||
<NarrowLayout style={{ width: '100%' }}>
|
||||
<InputBarContainer
|
||||
id="inputbar"
|
||||
className={classNames('inputbar-container', inputFocus && 'focus')}
|
||||
ref={containerRef}>
|
||||
<AttachmentPreview files={files} setFiles={setFiles} />
|
||||
<MentionModelsInput selectedModels={mentionModels} onRemoveModel={handleRemoveModel} />
|
||||
<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"
|
||||
spellCheck={false}
|
||||
rows={textareaRows}
|
||||
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.new_topic', { Command: newTopicShortcut })} arrow>
|
||||
<ToolbarButton type="text" onClick={addNewTopic}>
|
||||
<FormOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<MentionModelsButton
|
||||
mentionModels={mentionModels}
|
||||
onMentionModel={onMentionModel}
|
||||
ToolbarButton={ToolbarButton}
|
||||
/>
|
||||
{isWebSearchModel(model) && (
|
||||
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
|
||||
<ToolbarButton
|
||||
type="text"
|
||||
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}>
|
||||
<GlobalOutlined
|
||||
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }}
|
||||
/>
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip placement="top" title={t('chat.input.clear', { Command: cleanTopicShortcut })} 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.title')}>
|
||||
<ToolbarButton type="text">
|
||||
<ClearOutlined />
|
||||
</ToolbarButton>
|
||||
</Popconfirm>
|
||||
</Tooltip>
|
||||
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
|
||||
<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={textareaRows}
|
||||
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.new_topic', { Command: newTopicShortcut })} arrow>
|
||||
<ToolbarButton type="text" onClick={addNewTopic}>
|
||||
<FormOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
{isWebSearchModel(model) && (
|
||||
<Tooltip placement="top" title={t('chat.input.web_search')} arrow>
|
||||
<ToolbarButton
|
||||
type="text"
|
||||
onClick={() => {
|
||||
!showTopics && toggleShowTopics()
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
|
||||
}}>
|
||||
<ControlOutlined />
|
||||
onClick={() => updateAssistant({ ...assistant, enableWebSearch: !assistant.enableWebSearch })}>
|
||||
<GlobalOutlined
|
||||
style={{ color: assistant.enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)' }}
|
||||
/>
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
{showKnowledgeIcon && (
|
||||
<KnowledgeBaseButton
|
||||
selectedBase={selectedKnowledgeBase}
|
||||
onSelect={handleKnowledgeBaseSelect}
|
||||
ToolbarButton={ToolbarButton}
|
||||
disabled={files.length > 0}
|
||||
/>
|
||||
)}
|
||||
<AttachmentButton model={model} files={files} setFiles={setFiles} ToolbarButton={ToolbarButton} />
|
||||
<Tooltip placement="top" title={t('chat.input.new.context', { Command: newContextShortcut })} arrow>
|
||||
<ToolbarButton type="text" onClick={onNewContext}>
|
||||
<PicCenterOutlined />
|
||||
)}
|
||||
<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>
|
||||
<Tooltip placement="top" title={t('chat.input.settings')} arrow>
|
||||
<ToolbarButton
|
||||
type="text"
|
||||
onClick={() => {
|
||||
!showTopics && toggleShowTopics()
|
||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_CHAT_SETTINGS), 0)
|
||||
}}>
|
||||
<ControlOutlined />
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<KnowledgeBaseButton
|
||||
selectedBase={selectedKnowledgeBase}
|
||||
onSelect={handleKnowledgeBaseSelect}
|
||||
ToolbarButton={ToolbarButton}
|
||||
disabled={files.length > 0}
|
||||
/>
|
||||
<AttachmentButton
|
||||
model={model}
|
||||
files={files}
|
||||
setFiles={setFiles}
|
||||
ToolbarButton={ToolbarButton}
|
||||
disabled={!!selectedKnowledgeBase}
|
||||
/>
|
||||
<ToolbarButton type="text" onClick={onNewContext}>
|
||||
<Tooltip placement="top" title={t('chat.input.new.context')}>
|
||||
<PicCenterOutlined />
|
||||
</Tooltip>
|
||||
</ToolbarButton>
|
||||
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||
<ToolbarButton type="text" onClick={onToggleExpended}>
|
||||
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<TokenCount
|
||||
estimateTokenCount={estimateTokenCount}
|
||||
inputTokenCount={inputTokenCount}
|
||||
contextCount={contextCount}
|
||||
ToolbarButton={ToolbarButton}
|
||||
onClick={onNewContext}
|
||||
/>
|
||||
</ToolbarMenu>
|
||||
<ToolbarMenu>
|
||||
{!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>
|
||||
<Tooltip placement="top" title={expended ? t('chat.input.collapse') : t('chat.input.expand')} arrow>
|
||||
<ToolbarButton type="text" onClick={onToggleExpended}>
|
||||
{expended ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
<TokenCount
|
||||
estimateTokenCount={estimateTokenCount}
|
||||
inputTokenCount={inputTokenCount}
|
||||
contextCount={contextCount}
|
||||
ToolbarButton={ToolbarButton}
|
||||
onClick={onNewContext}
|
||||
/>
|
||||
</ToolbarMenu>
|
||||
<ToolbarMenu>
|
||||
{!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>
|
||||
</NarrowLayout>
|
||||
)}
|
||||
{!generating && <SendMessageButton sendMessage={sendMessage} disabled={generating || inputEmpty} />}
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
</InputBarContainer>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,6 @@ const KnowledgeBaseButton: FC<Props> = ({ selectedBase, onSelect, disabled, Tool
|
||||
<Popover
|
||||
placement="top"
|
||||
content={<KnowledgeBaseSelector selectedBase={selectedBase} onSelect={onSelect} />}
|
||||
overlayStyle={{ maxWidth: 400 }}
|
||||
trigger="click">
|
||||
<ToolbarButton type="text" onClick={() => selectedBase && onSelect(undefined)} disabled={disabled}>
|
||||
<FileSearchOutlined style={{ color: selectedBase ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { PushpinOutlined } from '@ant-design/icons'
|
||||
import ModelTags from '@renderer/components/ModelTags'
|
||||
import { getModelLogo, isEmbeddingModel } from '@renderer/config/models'
|
||||
import db from '@renderer/databases'
|
||||
import { useProviders } from '@renderer/hooks/useProvider'
|
||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||
import { Model } from '@renderer/types'
|
||||
import { Avatar, Dropdown, Tooltip } from 'antd'
|
||||
import { first, sortBy } from 'lodash'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled, { createGlobalStyle } from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
mentionModels: Model[]
|
||||
onMentionModel: (model: Model) => void
|
||||
ToolbarButton: any
|
||||
}
|
||||
|
||||
const MentionModelsButton: FC<Props> = ({ onMentionModel: onSelect, ToolbarButton }) => {
|
||||
const { providers } = useProviders()
|
||||
const [pinnedModels, setPinnedModels] = useState<string[]>([])
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const loadPinnedModels = async () => {
|
||||
const setting = await db.settings.get('pinned:models')
|
||||
setPinnedModels(setting?.value || [])
|
||||
}
|
||||
loadPinnedModels()
|
||||
}, [])
|
||||
|
||||
const togglePin = async (modelId: string) => {
|
||||
const newPinnedModels = pinnedModels.includes(modelId)
|
||||
? pinnedModels.filter((id) => id !== modelId)
|
||||
: [...pinnedModels, modelId]
|
||||
|
||||
await db.settings.put({ id: 'pinned:models', value: newPinnedModels })
|
||||
setPinnedModels(newPinnedModels)
|
||||
}
|
||||
|
||||
const modelMenuItems = providers
|
||||
.filter((p) => p.models && p.models.length > 0)
|
||||
.map((p) => {
|
||||
const filteredModels = sortBy(p.models, ['group', 'name'])
|
||||
.filter((m) => !isEmbeddingModel(m))
|
||||
.map((m) => ({
|
||||
key: getModelUniqId(m),
|
||||
label: (
|
||||
<ModelItem>
|
||||
<span>
|
||||
{m?.name} <ModelTags model={m} />
|
||||
</span>
|
||||
{/* <Checkbox checked={selectedModels.some((sm) => sm.id === m.id)} /> */}
|
||||
<PinIcon
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
togglePin(getModelUniqId(m))
|
||||
}}
|
||||
$isPinned={pinnedModels.includes(getModelUniqId(m))}>
|
||||
<PushpinOutlined />
|
||||
</PinIcon>
|
||||
</ModelItem>
|
||||
),
|
||||
icon: (
|
||||
<Avatar src={getModelLogo(m.id)} size={24}>
|
||||
{first(m.name)}
|
||||
</Avatar>
|
||||
),
|
||||
onClick: () => {
|
||||
onSelect(m)
|
||||
}
|
||||
}))
|
||||
|
||||
return filteredModels.length > 0
|
||||
? {
|
||||
key: p.id,
|
||||
label: p.isSystem ? t(`provider.${p.id}`) : p.name,
|
||||
type: 'group' as const,
|
||||
children: filteredModels
|
||||
}
|
||||
: null
|
||||
})
|
||||
.filter(Boolean)
|
||||
|
||||
if (pinnedModels.length > 0) {
|
||||
const pinnedItems = modelMenuItems
|
||||
.flatMap((p) => p?.children || [])
|
||||
.filter((m) => pinnedModels.includes(m.key))
|
||||
.map((m) => ({ ...m, key: m.key + 'pinned' }))
|
||||
|
||||
if (pinnedItems.length > 0) {
|
||||
modelMenuItems.unshift({
|
||||
key: 'pinned',
|
||||
label: t('models.pinned'),
|
||||
type: 'group' as const,
|
||||
children: pinnedItems
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuStyle />
|
||||
<Dropdown menu={{ items: modelMenuItems }} trigger={['click']} overlayClassName="mention-models-dropdown">
|
||||
<Tooltip placement="top" title={t('agents.edit.model.select.title')} arrow>
|
||||
<ToolbarButton type="text">
|
||||
<i className="iconfont icon-at" style={{ fontSize: 18 }}></i>
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const DropdownMenuStyle = createGlobalStyle`
|
||||
.mention-models-dropdown {
|
||||
.ant-dropdown-menu {
|
||||
max-height: 400px;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const ModelItem = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
gap: 16px;
|
||||
|
||||
&:hover {
|
||||
.pin-icon {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const PinIcon = styled.span.attrs({ className: 'pin-icon' })<{ $isPinned: boolean }>`
|
||||
margin-left: auto;
|
||||
padding: 0 8px;
|
||||
opacity: ${(props) => (props.$isPinned ? 1 : 'inherit')};
|
||||
transition: opacity 0.2s;
|
||||
right: 0;
|
||||
color: ${(props) => (props.$isPinned ? 'var(--color-primary)' : 'inherit')};
|
||||
transform: ${(props) => (props.$isPinned ? 'rotate(-45deg)' : 'none')};
|
||||
opacity: 0;
|
||||
|
||||
&:hover {
|
||||
opacity: 1 !important;
|
||||
color: ${(props) => (props.$isPinned ? 'var(--color-primary)' : 'inherit')};
|
||||
}
|
||||
`
|
||||
|
||||
export default MentionModelsButton
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Model } from '@renderer/types'
|
||||
import { Flex, Tag } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const MentionModelsInput: FC<{
|
||||
selectedModels: Model[]
|
||||
onRemoveModel: (model: Model) => void
|
||||
}> = ({ selectedModels, onRemoveModel }) => {
|
||||
return (
|
||||
<Container gap="4px 0" wrap>
|
||||
{selectedModels.map((model) => (
|
||||
<Tag bordered={false} color="processing" key={model.id} closable onClose={() => onRemoveModel(model)}>
|
||||
@{model.name}
|
||||
</Tag>
|
||||
))}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled(Flex)`
|
||||
width: 100%;
|
||||
padding: 10px 15px 0;
|
||||
`
|
||||
|
||||
export default MentionModelsInput
|
||||
@@ -66,9 +66,6 @@ const Container = styled.div`
|
||||
font-size: 10px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const Text = styled.div`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DownloadOutlined, ExpandOutlined } from '@ant-design/icons'
|
||||
import MinApp from '@renderer/components/MinApp'
|
||||
import { AppLogo } from '@renderer/config/env'
|
||||
import { extractTitle } from '@renderer/utils/formats'
|
||||
import { extractTitle } from '@renderer/utils/formula'
|
||||
import { Button } from 'antd'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { CheckOutlined, DownloadOutlined, DownOutlined, RightOutlined } from '@ant-design/icons'
|
||||
import { CheckOutlined, DownOutlined, RightOutlined } from '@ant-design/icons'
|
||||
import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useSyntaxHighlighter } from '@renderer/context/SyntaxHighlighterProvider'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import dayjs from 'dayjs'
|
||||
import React, { memo, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -18,6 +16,28 @@ interface CodeBlockProps {
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
const CollapseIcon: React.FC<{ expanded: boolean; onClick: () => void }> = ({ expanded, onClick }) => {
|
||||
return (
|
||||
<CollapseIconWrapper onClick={onClick}>
|
||||
{expanded ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
|
||||
</CollapseIconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const ExpandButton: React.FC<{
|
||||
isExpanded: boolean
|
||||
onClick: () => void
|
||||
showButton: boolean
|
||||
}> = ({ isExpanded, onClick, showButton }) => {
|
||||
if (!showButton) return null
|
||||
|
||||
return (
|
||||
<ExpandButtonWrapper onClick={onClick}>
|
||||
<div className="button-text">{isExpanded ? '收起' : '展开'}</div>
|
||||
</ExpandButtonWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||
const match = /language-(\w+)/.exec(className || '')
|
||||
const { codeShowLineNumbers, fontSize, codeCollapsible } = useSettings()
|
||||
@@ -30,8 +50,6 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||
|
||||
const showFooterCopyButton = children && children.length > 500 && !codeCollapsible
|
||||
|
||||
const showDownloadButton = ['csv', 'json', 'txt', 'md'].includes(language)
|
||||
|
||||
useEffect(() => {
|
||||
const loadHighlightedCode = async () => {
|
||||
const highlightedHtml = await codeToHtml(children, language)
|
||||
@@ -83,10 +101,7 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||
)}
|
||||
<CodeLanguage>{'<' + match[1].toUpperCase() + '>'}</CodeLanguage>
|
||||
</div>
|
||||
<HStack gap={12} alignItems="center">
|
||||
{showDownloadButton && <DownloadButton language={language} data={children} />}
|
||||
<CopyButton text={children} />
|
||||
</HStack>
|
||||
<CopyButton text={children} />
|
||||
</CodeHeader>
|
||||
<CodeContent
|
||||
ref={codeContentRef}
|
||||
@@ -122,28 +137,6 @@ const CodeBlock: React.FC<CodeBlockProps> = ({ children, className }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const CollapseIcon: React.FC<{ expanded: boolean; onClick: () => void }> = ({ expanded, onClick }) => {
|
||||
return (
|
||||
<CollapseIconWrapper onClick={onClick}>
|
||||
{expanded ? <DownOutlined style={{ fontSize: 12 }} /> : <RightOutlined style={{ fontSize: 12 }} />}
|
||||
</CollapseIconWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const ExpandButton: React.FC<{
|
||||
isExpanded: boolean
|
||||
onClick: () => void
|
||||
showButton: boolean
|
||||
}> = ({ isExpanded, onClick, showButton }) => {
|
||||
if (!showButton) return null
|
||||
|
||||
return (
|
||||
<ExpandButtonWrapper onClick={onClick}>
|
||||
<div className="button-text">{isExpanded ? '收起' : '展开'}</div>
|
||||
</ExpandButtonWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ text, style }) => {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
@@ -162,19 +155,6 @@ const CopyButton: React.FC<{ text: string; style?: React.CSSProperties }> = ({ t
|
||||
)
|
||||
}
|
||||
|
||||
const DownloadButton = ({ language, data }: { language: string; data: string }) => {
|
||||
const onDownload = () => {
|
||||
const fileName = `${dayjs().format('YYYYMMDDHHmm')}.${language}`
|
||||
window.api.file.save(fileName, data)
|
||||
}
|
||||
|
||||
return (
|
||||
<DownloadWrapper onClick={onDownload}>
|
||||
<DownloadOutlined />
|
||||
</DownloadWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
const CodeBlockWrapper = styled.div``
|
||||
|
||||
const CodeContent = styled.div<{ isShowLineNumbers: boolean }>`
|
||||
@@ -284,18 +264,4 @@ const CollapseIconWrapper = styled.div`
|
||||
}
|
||||
`
|
||||
|
||||
const DownloadWrapper = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: var(--color-text-3);
|
||||
transition: color 0.3s;
|
||||
font-size: 16px;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
`
|
||||
|
||||
export default memo(CodeBlock)
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'katex/dist/katex.min.css'
|
||||
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { Message } from '@renderer/types'
|
||||
import { escapeBrackets, removeSvgEmptyLines, withGeminiGrounding } from '@renderer/utils/formats'
|
||||
import { escapeBrackets, removeSvgEmptyLines } from '@renderer/utils/formula'
|
||||
import { isEmpty } from 'lodash'
|
||||
import { FC, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -34,9 +34,9 @@ const Markdown: FC<Props> = ({ message }) => {
|
||||
const messageContent = useMemo(() => {
|
||||
const empty = isEmpty(message.content)
|
||||
const paused = message.status === 'paused'
|
||||
const content = empty && paused ? t('message.chat.completion.paused') : withGeminiGrounding(message)
|
||||
const content = empty && paused ? t('message.chat.completion.paused') : message.content
|
||||
return removeSvgEmptyLines(escapeBrackets(content))
|
||||
}, [message, t])
|
||||
}, [message.content, message.status, t])
|
||||
|
||||
const rehypePlugins = useMemo(() => {
|
||||
const hasElements = ALLOWED_ELEMENTS.test(messageContent)
|
||||
|
||||
@@ -18,7 +18,6 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
const { t } = useTranslation()
|
||||
const mermaidId = `mermaid-popup-${Date.now()}`
|
||||
const [activeTab, setActiveTab] = useState('preview')
|
||||
const [scale, setScale] = useState(1)
|
||||
|
||||
const onOk = () => {
|
||||
setOpen(false)
|
||||
@@ -32,25 +31,6 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
resolve({})
|
||||
}
|
||||
|
||||
const handleZoom = (delta: number) => {
|
||||
const newScale = Math.max(0.1, Math.min(3, scale + delta))
|
||||
setScale(newScale)
|
||||
|
||||
const element = document.getElementById(mermaidId)
|
||||
if (!element) return
|
||||
|
||||
const svg = element.querySelector('svg')
|
||||
if (!svg) return
|
||||
|
||||
const container = svg.parentElement
|
||||
if (container) {
|
||||
container.style.overflow = 'auto'
|
||||
container.style.position = 'relative'
|
||||
svg.style.transformOrigin = 'top left'
|
||||
svg.style.transform = `scale(${newScale})`
|
||||
}
|
||||
}
|
||||
|
||||
const handleDownload = async (format: 'svg' | 'png') => {
|
||||
try {
|
||||
const element = document.getElementById(mermaidId)
|
||||
@@ -130,8 +110,6 @@ const PopupContainer: React.FC<Props> = ({ resolve, chart }) => {
|
||||
{activeTab === 'source' && <Button onClick={() => handleCopy()}>{t('common.copy')}</Button>}
|
||||
{activeTab === 'preview' && (
|
||||
<>
|
||||
<Button onClick={() => handleZoom(0.1)}>{t('mermaid.resize.zoom-in')}</Button>
|
||||
<Button onClick={() => handleZoom(-0.1)}>{t('mermaid.resize.zoom-out')}</Button>
|
||||
<Button onClick={() => handleDownload('svg')}>{t('mermaid.download.svg')}</Button>
|
||||
<Button onClick={() => handleDownload('png')}>{t('mermaid.download.png')}</Button>
|
||||
</>
|
||||
|
||||
@@ -109,8 +109,6 @@ const MessageItem: FC<Props> = ({
|
||||
if (topic && onGetMessages && onSetMessages) {
|
||||
if (message.status === 'sending') {
|
||||
const messages = onGetMessages()
|
||||
const assistantWithModel = message.model ? { ...assistant, model: message.model } : assistant
|
||||
|
||||
fetchChatCompletion({
|
||||
message,
|
||||
messages: messages
|
||||
@@ -119,7 +117,8 @@ const MessageItem: FC<Props> = ({
|
||||
0,
|
||||
messages.findIndex((m) => m.id === message.id)
|
||||
),
|
||||
assistant: assistantWithModel,
|
||||
assistant,
|
||||
topic,
|
||||
onResponse: (msg) => {
|
||||
setMessage(msg)
|
||||
if (msg.status !== 'pending') {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { SyncOutlined, TranslationOutlined } from '@ant-design/icons'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { getBriefInfo } from '@renderer/utils'
|
||||
import { Divider, Flex } from 'antd'
|
||||
import { Divider } from 'antd'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BeatLoader from 'react-spinners/BeatLoader'
|
||||
@@ -10,7 +10,6 @@ import styled from 'styled-components'
|
||||
import Markdown from '../Markdown/Markdown'
|
||||
import MessageAttachments from './MessageAttachments'
|
||||
import MessageError from './MessageError'
|
||||
import MessageSearchResults from './MessageSearchResults'
|
||||
|
||||
const MessageContent: React.FC<{
|
||||
message: Message
|
||||
@@ -37,9 +36,6 @@ const MessageContent: React.FC<{
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex gap="8px" wrap>
|
||||
{message.mentions?.map((model) => <MentionTag key={model.id}>{'@' + model.name}</MentionTag>)}
|
||||
</Flex>
|
||||
<Markdown message={message} />
|
||||
{message.translatedContent && (
|
||||
<>
|
||||
@@ -54,7 +50,6 @@ const MessageContent: React.FC<{
|
||||
</>
|
||||
)}
|
||||
<MessageAttachments message={message} />
|
||||
<MessageSearchResults message={message} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -68,8 +63,4 @@ const MessageContentLoading = styled.div`
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
const MentionTag = styled.span`
|
||||
color: var(--color-link);
|
||||
`
|
||||
|
||||
export default React.memo(MessageContent)
|
||||
|
||||
@@ -27,7 +27,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
|
||||
const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
||||
const avatar = useAvatar()
|
||||
const { theme } = useTheme()
|
||||
const { userName, sidebarIcons } = useSettings()
|
||||
const { userName } = useSettings()
|
||||
const { t } = useTranslation()
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
|
||||
@@ -40,14 +40,11 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
||||
}, [message.modelId, message.role, model?.id, model?.name, t, userName])
|
||||
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
const showMinappIcon = sidebarIcons.visible.includes('minapp')
|
||||
|
||||
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
|
||||
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
|
||||
|
||||
const showMiniApp = useCallback(() => {
|
||||
showMinappIcon && model?.provider && startMinAppById(model.provider)
|
||||
}, [model?.provider, showMinappIcon])
|
||||
const showMiniApp = useCallback(() => model?.provider && startMinAppById(model.provider), [model?.provider])
|
||||
|
||||
const avatarStyle: CSSProperties | undefined = isBubbleStyle
|
||||
? {
|
||||
@@ -57,7 +54,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<Container className="message-header">
|
||||
<Container>
|
||||
<AvatarWrapper style={avatarStyle}>
|
||||
{isAssistantMessage ? (
|
||||
<Avatar
|
||||
@@ -65,7 +62,7 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
|
||||
size={35}
|
||||
style={{
|
||||
borderRadius: '20%',
|
||||
cursor: showMinappIcon ? 'pointer' : 'default',
|
||||
cursor: 'pointer',
|
||||
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
|
||||
filter: theme === 'dark' ? 'invert(0.05)' : undefined
|
||||
}}
|
||||
|
||||
@@ -11,11 +11,10 @@ import {
|
||||
} from '@ant-design/icons'
|
||||
import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { translateText } from '@renderer/services/TranslateService'
|
||||
import { Message, Model } from '@renderer/types'
|
||||
import { removeTrailingDoubleSpaces, uuid } from '@renderer/utils'
|
||||
import { removeTrailingDoubleSpaces } from '@renderer/utils'
|
||||
import { Button, Dropdown, Popconfirm, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { FC, useCallback, useMemo, useState } from 'react'
|
||||
@@ -70,8 +69,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
[setModel]
|
||||
)
|
||||
|
||||
const onNewBranch = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
const onNewBranch = useCallback(() => {
|
||||
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
|
||||
window.message.success({
|
||||
content: t('chat.message.new.branch.created'),
|
||||
@@ -79,8 +77,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
})
|
||||
}, [index, t])
|
||||
|
||||
const onResend = useCallback(async () => {
|
||||
await modelGenerating()
|
||||
const onResend = useCallback(() => {
|
||||
const _messages = onGetMessages?.() || []
|
||||
const index = _messages.findIndex((m) => m.id === message.id)
|
||||
const nextIndex = index + 1
|
||||
@@ -95,12 +92,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
translatedContent: undefined
|
||||
})
|
||||
}
|
||||
|
||||
if (!nextMessage || nextMessage.role === 'user') {
|
||||
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, { ...message, id: uuid() })
|
||||
onDeleteMessage?.(message)
|
||||
}
|
||||
}, [assistantModel?.id, message, model?.id, onDeleteMessage, onGetMessages])
|
||||
}, [assistantModel?.id, message.id, model?.id, onGetMessages])
|
||||
|
||||
const onEdit = useCallback(async () => {
|
||||
let resendMessage = false
|
||||
@@ -166,23 +158,57 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
onClick: onEdit
|
||||
},
|
||||
{
|
||||
label: t('chat.message.new.branch'),
|
||||
key: 'new-branch',
|
||||
icon: <ForkOutlined />,
|
||||
onClick: onNewBranch
|
||||
label: t('chat.translate'),
|
||||
key: 'translate',
|
||||
icon: isTranslating ? <SyncOutlined spin /> : <TranslationOutlined />,
|
||||
children: [
|
||||
{
|
||||
label: '🇨🇳 ' + t('languages.chinese'),
|
||||
key: 'translate-chinese',
|
||||
onClick: () => handleTranslate('chinese')
|
||||
},
|
||||
{
|
||||
label: '🇭🇰 ' + t('languages.chinese-traditional'),
|
||||
key: 'translate-chinese-traditional',
|
||||
onClick: () => handleTranslate('chinese-traditional')
|
||||
},
|
||||
{
|
||||
label: '🇬🇧 ' + t('languages.english'),
|
||||
key: 'translate-english',
|
||||
onClick: () => handleTranslate('english')
|
||||
},
|
||||
{
|
||||
label: '🇯🇵 ' + t('languages.japanese'),
|
||||
key: 'translate-japanese',
|
||||
onClick: () => handleTranslate('japanese')
|
||||
},
|
||||
{
|
||||
label: '🇰🇷 ' + t('languages.korean'),
|
||||
key: 'translate-korean',
|
||||
onClick: () => handleTranslate('korean')
|
||||
},
|
||||
{
|
||||
label: '🇷🇺 ' + t('languages.russian'),
|
||||
key: 'translate-russian',
|
||||
onClick: () => handleTranslate('russian')
|
||||
},
|
||||
{
|
||||
label: '✖ ' + t('translate.close'),
|
||||
key: 'translate-close',
|
||||
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[message, onEdit, onNewBranch, t]
|
||||
[handleTranslate, isTranslating, message, onEdit, onEditMessage, t]
|
||||
)
|
||||
|
||||
const onAtModelRegenerate = async () => {
|
||||
await modelGenerating()
|
||||
const selectedModel = await SelectModelPopup.show({ model })
|
||||
selectedModel && onRegenerate(selectedModel)
|
||||
}
|
||||
|
||||
const onDeleteAndRegenerate = async () => {
|
||||
await modelGenerating()
|
||||
const onDeleteAndRegenerate = () => {
|
||||
onEditMessage?.({
|
||||
...message,
|
||||
content: '',
|
||||
@@ -195,7 +221,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
return (
|
||||
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
|
||||
<Tooltip title="Edit" mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onEdit}>
|
||||
<EditOutlined />
|
||||
</ActionButton>
|
||||
@@ -224,60 +250,16 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
{canRegenerate && (
|
||||
<Tooltip title={t('chat.message.regenerate.model')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onAtModelRegenerate}>
|
||||
<i className="iconfont icon-at"></i>
|
||||
<i className="iconfont icon-at1"></i>
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!isUserMessage && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{
|
||||
label: '🇨🇳 ' + t('languages.chinese'),
|
||||
key: 'translate-chinese',
|
||||
onClick: () => handleTranslate('chinese')
|
||||
},
|
||||
{
|
||||
label: '🇭🇰 ' + t('languages.chinese-traditional'),
|
||||
key: 'translate-chinese-traditional',
|
||||
onClick: () => handleTranslate('chinese-traditional')
|
||||
},
|
||||
{
|
||||
label: '🇬🇧 ' + t('languages.english'),
|
||||
key: 'translate-english',
|
||||
onClick: () => handleTranslate('english')
|
||||
},
|
||||
{
|
||||
label: '🇯🇵 ' + t('languages.japanese'),
|
||||
key: 'translate-japanese',
|
||||
onClick: () => handleTranslate('japanese')
|
||||
},
|
||||
{
|
||||
label: '🇰🇷 ' + t('languages.korean'),
|
||||
key: 'translate-korean',
|
||||
onClick: () => handleTranslate('korean')
|
||||
},
|
||||
{
|
||||
label: '🇷🇺 ' + t('languages.russian'),
|
||||
key: 'translate-russian',
|
||||
onClick: () => handleTranslate('russian')
|
||||
},
|
||||
{
|
||||
label: '✖ ' + t('translate.close'),
|
||||
key: 'translate-close',
|
||||
onClick: () => onEditMessage?.({ ...message, translatedContent: undefined })
|
||||
}
|
||||
]
|
||||
}}
|
||||
trigger={['click']}
|
||||
placement="topRight"
|
||||
arrow>
|
||||
<Tooltip title={t('chat.translate')} mouseEnterDelay={1.2}>
|
||||
<ActionButton className="message-action-button">
|
||||
<TranslationOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Dropdown>
|
||||
{isAssistantMessage && (
|
||||
<Tooltip title={t('chat.message.new.branch')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onNewBranch}>
|
||||
<ForkOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popconfirm
|
||||
title={t('message.message.delete.content')}
|
||||
@@ -335,7 +317,7 @@ const ActionButton = styled.div`
|
||||
&:hover {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
.icon-at {
|
||||
.icon-at1 {
|
||||
font-size: 16px;
|
||||
}
|
||||
`
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { Message } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props {
|
||||
message: Message
|
||||
}
|
||||
|
||||
const MessageSearchResults: FC<Props> = ({ message }) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (!message.metadata?.groundingMetadata) {
|
||||
return null
|
||||
}
|
||||
|
||||
const { groundingChunks, searchEntryPoint } = message.metadata.groundingMetadata
|
||||
|
||||
if (!groundingChunks) {
|
||||
return null
|
||||
}
|
||||
|
||||
let searchEntryContent = searchEntryPoint?.renderedContent
|
||||
|
||||
searchEntryContent = searchEntryContent?.replace(
|
||||
/@media \(prefers-color-scheme: light\)/g,
|
||||
'body[theme-mode="light"]'
|
||||
)
|
||||
|
||||
searchEntryContent = searchEntryContent?.replace(/@media \(prefers-color-scheme: dark\)/g, 'body[theme-mode="dark"]')
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container className="footnotes">
|
||||
<TitleRow>
|
||||
<Title>{t('common.footnotes')}</Title>
|
||||
<InfoCircleOutlined />
|
||||
</TitleRow>
|
||||
<Sources>
|
||||
{groundingChunks.map((chunk, index) => (
|
||||
<SourceItem key={index}>
|
||||
<Link href={chunk.web?.uri} target="_blank" rel="noopener noreferrer">
|
||||
{chunk.web?.title}
|
||||
</Link>
|
||||
</SourceItem>
|
||||
))}
|
||||
</Sources>
|
||||
</Container>
|
||||
<SearchEntryPoint dangerouslySetInnerHTML={{ __html: searchEntryContent || '' }} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 0;
|
||||
`
|
||||
|
||||
const TitleRow = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
const Title = styled.h4`
|
||||
margin: 0 !important;
|
||||
`
|
||||
|
||||
const Sources = styled.ol`
|
||||
margin-top: 10px;
|
||||
`
|
||||
|
||||
const SourceItem = styled.li`
|
||||
margin-bottom: 5px;
|
||||
`
|
||||
|
||||
const Link = styled.a`
|
||||
margin-left: 5px;
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`
|
||||
|
||||
const SearchEntryPoint = styled.div`
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
`
|
||||
|
||||
export default MessageSearchResults
|
||||
@@ -26,7 +26,6 @@ import styled from 'styled-components'
|
||||
|
||||
import Suggestions from '../components/Suggestions'
|
||||
import MessageItem from './Message'
|
||||
import NarrowLayout from './NarrowLayout'
|
||||
import Prompt from './Prompt'
|
||||
|
||||
interface Props {
|
||||
@@ -97,19 +96,10 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
|
||||
const onSendMessage = useCallback(
|
||||
async (message: Message) => {
|
||||
const assistantMessages: Message[] = []
|
||||
if (message.mentions?.length) {
|
||||
message.mentions.forEach((m) => {
|
||||
const assistantMessage = getAssistantMessage({ assistant: { ...assistant, model: m }, topic })
|
||||
assistantMessage.model = m
|
||||
assistantMessages.push(assistantMessage)
|
||||
})
|
||||
} else {
|
||||
assistantMessages.push(getAssistantMessage({ assistant, topic }))
|
||||
}
|
||||
const assistantMessage = getAssistantMessage({ assistant, topic })
|
||||
|
||||
setMessages((prev) => {
|
||||
const messages = prev.concat([message, ...assistantMessages])
|
||||
const messages = prev.concat([message, assistantMessage])
|
||||
db.topics.put({ id: topic.id, messages })
|
||||
return messages
|
||||
})
|
||||
@@ -146,7 +136,6 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
(message: Message) => {
|
||||
const _messages = messages.filter((m) => m.id !== message.id)
|
||||
setMessages(_messages)
|
||||
setDisplayMessages(_messages)
|
||||
db.topics.update(topic.id, { messages: _messages })
|
||||
deleteMessageFiles(message)
|
||||
},
|
||||
@@ -165,8 +154,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.REGENERATE_MESSAGE, async (model: Model) => {
|
||||
const lastUserMessage = last(filterMessages(messages).filter((m) => m.role === 'user'))
|
||||
lastUserMessage &&
|
||||
onSendMessage({ ...lastUserMessage, id: uuid(), modelId: model.id, model: model, mentions: [model] })
|
||||
lastUserMessage && onSendMessage({ ...lastUserMessage, id: uuid(), type: '@', modelId: model.id })
|
||||
}),
|
||||
EventEmitter.on(EVENT_NAMES.AI_AUTO_RENAME, autoRenameTopic),
|
||||
EventEmitter.on(EVENT_NAMES.CLEAR_MESSAGES, () => {
|
||||
@@ -294,35 +282,33 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
||||
key={assistant.id}
|
||||
ref={containerRef}
|
||||
right={topicPosition === 'left'}>
|
||||
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
|
||||
<Suggestions assistant={assistant} messages={messages} />
|
||||
<InfiniteScroll
|
||||
dataLength={displayMessages.length}
|
||||
next={loadMoreMessages}
|
||||
hasMore={hasMore}
|
||||
loader={null}
|
||||
inverse={true}
|
||||
scrollableTarget="messages">
|
||||
<ScrollContainer>
|
||||
<LoaderContainer $loading={isLoadingMore}>
|
||||
<BeatLoader size={8} color="var(--color-text-2)" />
|
||||
</LoaderContainer>
|
||||
{displayMessages.map((message, index) => (
|
||||
<MessageItem
|
||||
key={message.id}
|
||||
message={message}
|
||||
topic={topic}
|
||||
index={index}
|
||||
hidePresetMessages={assistant.settings?.hideMessages}
|
||||
onSetMessages={setMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
</InfiniteScroll>
|
||||
<Prompt assistant={assistant} key={assistant.prompt} />
|
||||
</NarrowLayout>
|
||||
<Suggestions assistant={assistant} messages={messages} />
|
||||
<InfiniteScroll
|
||||
dataLength={displayMessages.length}
|
||||
next={loadMoreMessages}
|
||||
hasMore={hasMore}
|
||||
loader={null}
|
||||
inverse={true}
|
||||
scrollableTarget="messages">
|
||||
<ScrollContainer>
|
||||
<LoaderContainer $loading={isLoadingMore}>
|
||||
<BeatLoader size={8} color="var(--color-text-2)" />
|
||||
</LoaderContainer>
|
||||
{displayMessages.map((message, index) => (
|
||||
<MessageItem
|
||||
key={message.id}
|
||||
message={message}
|
||||
topic={topic}
|
||||
index={index}
|
||||
hidePresetMessages={assistant.settings?.hideMessages}
|
||||
onSetMessages={setMessages}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
onGetMessages={onGetMessages}
|
||||
/>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
</InfiniteScroll>
|
||||
<Prompt assistant={assistant} key={assistant.prompt} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { FC, HTMLAttributes } from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const NarrowLayout: FC<Props> = ({ children, ...props }) => {
|
||||
const { narrowMode } = useSettings()
|
||||
|
||||
if (narrowMode) {
|
||||
return <Container {...props}>{children}</Container>
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
const Container = styled.div`
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
`
|
||||
|
||||
export default NarrowLayout
|
||||
@@ -28,7 +28,7 @@ const Container = styled.div`
|
||||
padding: 10px 20px;
|
||||
background-color: var(--color-background-soft);
|
||||
margin-bottom: 20px;
|
||||
margin: 4px 20px 0 20px;
|
||||
margin: 0 20px 0 20px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
border: 0.5px solid var(--color-border);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FormOutlined, SearchOutlined } from '@ant-design/icons'
|
||||
import { Navbar, NavbarLeft, NavbarRight } from '@renderer/components/app/Navbar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import MinAppsPopover from '@renderer/components/Popups/MinAppsPopover'
|
||||
import AppStorePopover from '@renderer/components/Popups/AppStorePopover'
|
||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||
import { isMac, isWindows } from '@renderer/config/constant'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
@@ -10,8 +10,6 @@ import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||
import { useShowAssistants, useShowTopics } from '@renderer/hooks/useStore'
|
||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setNarrowMode } from '@renderer/store/settings'
|
||||
import { Assistant, Topic } from '@renderer/types'
|
||||
import { FC } from 'react'
|
||||
import styled from 'styled-components'
|
||||
@@ -27,9 +25,8 @@ interface Props {
|
||||
const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
const { assistant } = useAssistant(activeAssistant.id)
|
||||
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||
const { topicPosition, sidebarIcons, narrowMode } = useSettings()
|
||||
const { topicPosition } = useSettings()
|
||||
const { showTopics, toggleShowTopics } = useShowTopics()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
useShortcut('toggle_show_assistants', () => {
|
||||
toggleShowAssistants()
|
||||
@@ -43,15 +40,11 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
}
|
||||
})
|
||||
|
||||
useShortcut('search_message', () => {
|
||||
SearchPopup.show()
|
||||
})
|
||||
|
||||
return (
|
||||
<Navbar className="home-navbar">
|
||||
<Navbar>
|
||||
{showAssistants && (
|
||||
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: 0 }}>
|
||||
<NavbarIcon onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 16 : 0 }}>
|
||||
<NavbarLeft style={{ justifyContent: 'space-between', borderRight: 'none', padding: '0 8px' }}>
|
||||
<NavbarIcon onClick={toggleShowAssistants} style={{ marginLeft: isMac ? 8 : 0 }}>
|
||||
<i className="iconfont icon-hide-sidebar" />
|
||||
</NavbarIcon>
|
||||
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
|
||||
@@ -59,12 +52,12 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
</NavbarIcon>
|
||||
</NavbarLeft>
|
||||
)}
|
||||
<NavbarRight
|
||||
style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}
|
||||
className="home-navbar-right">
|
||||
<NavbarRight style={{ justifyContent: 'space-between', paddingRight: isWindows ? 140 : 12, flex: 1 }}>
|
||||
<HStack alignItems="center">
|
||||
{!showAssistants && (
|
||||
<NavbarIcon onClick={() => toggleShowAssistants()} style={{ marginRight: 8, marginLeft: isMac ? 4 : -12 }}>
|
||||
<NavbarIcon
|
||||
onClick={() => toggleShowAssistants()}
|
||||
style={{ marginRight: isMac ? 8 : 25, marginLeft: isMac ? 4 : 0 }}>
|
||||
<i className="iconfont icon-show-sidebar" />
|
||||
</NavbarIcon>
|
||||
)}
|
||||
@@ -76,24 +69,19 @@ const HeaderNavbar: FC<Props> = ({ activeAssistant }) => {
|
||||
</TitleText>
|
||||
<SelectModelButton assistant={assistant} />
|
||||
</HStack>
|
||||
<HStack alignItems="center" gap={8}>
|
||||
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||
<HStack alignItems="center">
|
||||
<NavbarIcon onClick={() => SearchPopup.show()}>
|
||||
<SearchOutlined />
|
||||
</NarrowIcon>
|
||||
<NarrowIcon onClick={() => dispatch(setNarrowMode(!narrowMode))}>
|
||||
<i className="iconfont icon-icon-adaptive-width"></i>
|
||||
</NarrowIcon>
|
||||
{sidebarIcons.visible.includes('minapp') && (
|
||||
<MinAppsPopover>
|
||||
<NarrowIcon>
|
||||
<i className="iconfont icon-appstore" />
|
||||
</NarrowIcon>
|
||||
</MinAppsPopover>
|
||||
)}
|
||||
</NavbarIcon>
|
||||
<AppStorePopover>
|
||||
<NavbarIcon style={{ marginLeft: isMac ? 5 : 10 }}>
|
||||
<i className="iconfont icon-appstore" />
|
||||
</NavbarIcon>
|
||||
</AppStorePopover>
|
||||
{topicPosition === 'right' && (
|
||||
<NarrowIcon onClick={toggleShowTopics}>
|
||||
<NavbarIcon onClick={toggleShowTopics} style={{ marginLeft: isMac ? 5 : 10 }}>
|
||||
<i className={`iconfont icon-${showTopics ? 'show' : 'hide'}-sidebar`} />
|
||||
</NarrowIcon>
|
||||
</NavbarIcon>
|
||||
)}
|
||||
</HStack>
|
||||
</NavbarRight>
|
||||
@@ -138,17 +126,8 @@ export const NavbarIcon = styled.div`
|
||||
const TitleText = styled.span`
|
||||
margin-left: 5px;
|
||||
font-family: Ubuntu;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
@media (max-width: 1080px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
const NarrowIcon = styled(NavbarIcon)`
|
||||
@media (max-width: 1000px) {
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
|
||||
export default HeaderNavbar
|
||||
|
||||
@@ -4,11 +4,11 @@ import CopyIcon from '@renderer/components/Icons/CopyIcon'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useAgents } from '@renderer/hooks/useAgents'
|
||||
import { useAssistant, useAssistants } from '@renderer/hooks/useAssistant'
|
||||
import { modelGenerating } from '@renderer/hooks/useRuntime'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
import { useAppSelector } from '@renderer/store'
|
||||
import { Assistant } from '@renderer/types'
|
||||
import { uuid } from '@renderer/utils'
|
||||
import { Dropdown } from 'antd'
|
||||
@@ -32,6 +32,7 @@ const Assistants: FC<Props> = ({
|
||||
onCreateDefaultAssistant
|
||||
}) => {
|
||||
const { assistants, removeAssistant, addAssistant, updateAssistants } = useAssistants()
|
||||
const generating = useAppSelector((state) => state.runtime.generating)
|
||||
const [dragging, setDragging] = useState(false)
|
||||
const { removeAllTopics } = useAssistant(activeAssistant.id)
|
||||
const { clickAssistantToShowTopic, topicPosition } = useSettings()
|
||||
@@ -40,7 +41,7 @@ const Assistants: FC<Props> = ({
|
||||
|
||||
const onDelete = useCallback(
|
||||
(assistant: Assistant) => {
|
||||
const _assistant: Assistant | undefined = last(assistants.filter((a) => a.id !== assistant.id))
|
||||
const _assistant = last(assistants.filter((a) => a.id !== assistant.id))
|
||||
_assistant ? setActiveAssistant(_assistant) : onCreateDefaultAssistant()
|
||||
removeAssistant(assistant.id)
|
||||
},
|
||||
@@ -116,8 +117,13 @@ const Assistants: FC<Props> = ({
|
||||
)
|
||||
|
||||
const onSwitchAssistant = useCallback(
|
||||
async (assistant: Assistant) => {
|
||||
await modelGenerating()
|
||||
(assistant: Assistant): any => {
|
||||
if (generating) {
|
||||
return window.message.warning({
|
||||
content: t('message.switch.disabled'),
|
||||
key: 'switch-assistant'
|
||||
})
|
||||
}
|
||||
|
||||
if (topicPosition === 'left' && clickAssistantToShowTopic) {
|
||||
EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR)
|
||||
@@ -125,11 +131,11 @@ const Assistants: FC<Props> = ({
|
||||
|
||||
setActiveAssistant(assistant)
|
||||
},
|
||||
[clickAssistantToShowTopic, setActiveAssistant, topicPosition]
|
||||
[clickAssistantToShowTopic, generating, setActiveAssistant, t, topicPosition]
|
||||
)
|
||||
|
||||
return (
|
||||
<Container className="assistants-tab">
|
||||
<Container>
|
||||
<DragableList
|
||||
list={assistants}
|
||||
onUpdate={updateAssistants}
|
||||
@@ -181,7 +187,7 @@ const AssistantItem = styled.div`
|
||||
margin: 0 10px;
|
||||
padding-right: 35px;
|
||||
font-family: Ubuntu;
|
||||
border-radius: var(--list-item-border-radius);
|
||||
border-radius: 16px;
|
||||
border: 0.5px solid transparent;
|
||||
cursor: pointer;
|
||||
.iconfont {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CheckOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { CheckOutlined, DeleteOutlined, PlusOutlined, QuestionCircleOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import {
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
setShowMessageDivider
|
||||
} from '@renderer/store/settings'
|
||||
import { Assistant, AssistantSettings, ThemeMode } from '@renderer/types'
|
||||
import { Col, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { Button, Col, Input, InputNumber, Row, Select, Slider, Switch, Tooltip } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@@ -117,7 +117,7 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
}, [assistant])
|
||||
|
||||
return (
|
||||
<Container className="settings-tab">
|
||||
<Container>
|
||||
<SettingGroup style={{ marginTop: 10 }}>
|
||||
<SettingSubtitle style={{ marginTop: 0 }}>
|
||||
{t('settings.messages.model.title')}{' '}
|
||||
@@ -203,6 +203,106 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
{assistant?.settings?.customParameters?.map((param, index) => (
|
||||
<ParameterCard key={index}>
|
||||
<Row align="middle" gutter={8} style={{ marginBottom: 8 }}>
|
||||
<Col span={14}>
|
||||
<Input
|
||||
placeholder={t('models.parameter_name')}
|
||||
value={param.name}
|
||||
onChange={(e) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, name: e.target.value }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Select
|
||||
value={param.type}
|
||||
onChange={(value: 'string' | 'number' | 'boolean') => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
let defaultValue: any = ''
|
||||
switch (value) {
|
||||
case 'number':
|
||||
defaultValue = 0
|
||||
break
|
||||
case 'boolean':
|
||||
defaultValue = false
|
||||
break
|
||||
default:
|
||||
defaultValue = ''
|
||||
}
|
||||
newParams[index] = { ...param, type: value, value: defaultValue }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
style={{ width: '100%' }}>
|
||||
<Select.Option value="string">{t('models.parameter_type.string')}</Select.Option>
|
||||
<Select.Option value="number">{t('models.parameter_type.number')}</Select.Option>
|
||||
<Select.Option value="boolean">{t('models.parameter_type.boolean')}</Select.Option>
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row align="middle" gutter={10}>
|
||||
<Col span={20}>
|
||||
{param.type === 'boolean' ? (
|
||||
<Switch
|
||||
checked={param.value as boolean}
|
||||
onChange={(checked) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, value: checked }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
/>
|
||||
) : param.type === 'number' ? (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
value={param.value as number}
|
||||
onChange={(value) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, value: value || 0 }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
step={0.01}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={typeof param.value === 'string' ? param.value : JSON.stringify(param.value)}
|
||||
onChange={(e) => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams[index] = { ...param, value: e.target.value }
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Col>
|
||||
<Col span={4}>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => {
|
||||
const newParams = [...(assistant?.settings?.customParameters || [])]
|
||||
newParams.splice(index, 1)
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
danger
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</ParameterCard>
|
||||
))}
|
||||
<Button
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => {
|
||||
const newParams = [
|
||||
...(assistant?.settings?.customParameters || []),
|
||||
{ name: '', value: '', type: 'string' as const }
|
||||
]
|
||||
onUpdateAssistantSettings({ customParameters: newParams })
|
||||
}}
|
||||
style={{ marginBottom: 0, width: '100%', borderStyle: 'dashed' }}>
|
||||
{t('models.add_parameter')}
|
||||
</Button>
|
||||
</SettingGroup>
|
||||
<SettingGroup>
|
||||
<SettingSubtitle style={{ marginTop: 0 }}>{t('settings.messages.title')}</SettingSubtitle>
|
||||
@@ -391,7 +491,6 @@ const Container = styled(Scrollbar)`
|
||||
padding: 0 10px;
|
||||
padding-right: 5px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 10px;
|
||||
`
|
||||
|
||||
const Label = styled.p`
|
||||
@@ -411,11 +510,24 @@ const SettingRowTitleSmall = styled(SettingRowTitle)`
|
||||
`
|
||||
|
||||
export const SettingGroup = styled.div<{ theme?: ThemeMode }>`
|
||||
padding: 0 5px;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 10px;
|
||||
border: 0.5px solid var(--color-border);
|
||||
background: var(--color-group-background);
|
||||
`
|
||||
|
||||
const ParameterCard = styled.div`
|
||||
margin-bottom: 8px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
background: var(--color-background);
|
||||
&:last-child {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
`
|
||||
|
||||
export default SettingsTab
|
||||
|
||||