Compare commits
85 Commits
copilot/fi
...
feat/sideb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4434eb7c8 | ||
|
|
762732af9d | ||
|
|
5d9b47198b | ||
|
|
db1c03f9fa | ||
|
|
bedea8aaaa | ||
|
|
27d959caed | ||
|
|
0609b93a14 | ||
|
|
c7a0b05841 | ||
|
|
5ca0ce682b | ||
|
|
7c3752a8e6 | ||
|
|
7ae10be387 | ||
|
|
7767dfaac7 | ||
|
|
c89ff17b36 | ||
|
|
0810a63fd8 | ||
|
|
ff261fb52b | ||
|
|
8f8deb9275 | ||
|
|
0a7e591f0e | ||
|
|
c1e8f1063a | ||
|
|
4317f4b672 | ||
|
|
202504fd17 | ||
|
|
05727c637f | ||
|
|
c7843ca288 | ||
|
|
facf29e02b | ||
|
|
66d280136c | ||
|
|
91892ea619 | ||
|
|
7d70425c75 | ||
|
|
20b3db0c01 | ||
|
|
f1804bc3a0 | ||
|
|
2ec0c29087 | ||
|
|
20a572aa46 | ||
|
|
142b624001 | ||
|
|
3775562956 | ||
|
|
3544c40d9a | ||
|
|
36fa3af9e9 | ||
|
|
eb832cc25a | ||
|
|
0378f9ceb1 | ||
|
|
99b23e1d5d | ||
|
|
8d23c810fe | ||
|
|
cdfa2ac13a | ||
|
|
58f3edb352 | ||
|
|
edeb9f84f9 | ||
|
|
2757fcf6b9 | ||
|
|
5339f4a9a3 | ||
|
|
e786feb165 | ||
|
|
5794c36d0d | ||
|
|
7d8b88c56e | ||
|
|
d0daeddf14 | ||
|
|
2fe2ae797b | ||
|
|
9bb66227b4 | ||
|
|
5f4736e8c1 | ||
|
|
7a44910847 | ||
|
|
509632030b | ||
|
|
09fe2aa67b | ||
|
|
793c641e1c | ||
|
|
38eb206a8a | ||
|
|
3d9236a09a | ||
|
|
e6fd9b5678 | ||
|
|
d41f175a05 | ||
|
|
6d6a554fd3 | ||
|
|
cb1fcf7d2d | ||
|
|
6ed30fd78a | ||
|
|
69acb2fccd | ||
|
|
d95a4e56f5 | ||
|
|
b15dac9ef4 | ||
|
|
73fced37b4 | ||
|
|
1124090d87 | ||
|
|
a92fa1b1ba | ||
|
|
13fdfc58b6 | ||
|
|
048a9135ac | ||
|
|
b5636646c9 | ||
|
|
d9def89ced | ||
|
|
b16d0069bf | ||
|
|
30823691f9 | ||
|
|
8be98ccbb3 | ||
|
|
922e85754a | ||
|
|
f041f9a231 | ||
|
|
0d9f1882b9 | ||
|
|
26d823e0a5 | ||
|
|
daa89df479 | ||
|
|
527740bf42 | ||
|
|
881e0b4713 | ||
|
|
88e251cee7 | ||
|
|
76387643f7 | ||
|
|
b41c89972b | ||
|
|
11a8154458 |
@@ -176,6 +176,7 @@
|
|||||||
"eslint-plugin-unused-imports": "^4.1.4",
|
"eslint-plugin-unused-imports": "^4.1.4",
|
||||||
"fast-diff": "^1.3.0",
|
"fast-diff": "^1.3.0",
|
||||||
"fast-xml-parser": "^5.2.0",
|
"fast-xml-parser": "^5.2.0",
|
||||||
|
"framer-motion": "^12.17.3",
|
||||||
"franc-min": "^6.2.0",
|
"franc-min": "^6.2.0",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"google-auth-library": "^9.15.1",
|
"google-auth-library": "^9.15.1",
|
||||||
@@ -206,8 +207,8 @@
|
|||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-router": "6",
|
"react-router": "^7.6.2",
|
||||||
"react-router-dom": "6",
|
"react-router-dom": "^7.6.2",
|
||||||
"react-spinners": "^0.14.1",
|
"react-spinners": "^0.14.1",
|
||||||
"react-window": "^1.8.11",
|
"react-window": "^1.8.11",
|
||||||
"redux": "^5.0.1",
|
"redux": "^5.0.1",
|
||||||
|
|||||||
@@ -242,5 +242,12 @@ export enum IpcChannel {
|
|||||||
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
|
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
|
||||||
Selection_ActionWindowPin = 'selection:action-window-pin',
|
Selection_ActionWindowPin = 'selection:action-window-pin',
|
||||||
Selection_ProcessAction = 'selection:process-action',
|
Selection_ProcessAction = 'selection:process-action',
|
||||||
Selection_UpdateActionData = 'selection:update-action-data'
|
Selection_UpdateActionData = 'selection:update-action-data',
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
Navigation_Url = 'navigation:url',
|
||||||
|
Navigation_Close = 'navigation:close',
|
||||||
|
|
||||||
|
// Settings Window
|
||||||
|
SettingsWindow_Show = 'settings-window:show'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ if (isDev) {
|
|||||||
export const DATA_PATH = getDataPath()
|
export const DATA_PATH = getDataPath()
|
||||||
|
|
||||||
export const titleBarOverlayDark = {
|
export const titleBarOverlayDark = {
|
||||||
height: 40,
|
height: 42,
|
||||||
color: 'rgba(255,255,255,0)',
|
color: 'rgba(255,255,255,0)',
|
||||||
symbolColor: '#fff'
|
symbolColor: '#fff'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const titleBarOverlayLight = {
|
export const titleBarOverlayLight = {
|
||||||
height: 40,
|
height: 42,
|
||||||
color: 'rgba(255,255,255,0)',
|
color: 'rgba(255,255,255,0)',
|
||||||
symbolColor: '#000'
|
symbolColor: '#000'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { Notification } from 'src/renderer/src/types/notification'
|
|||||||
import appService from './services/AppService'
|
import appService from './services/AppService'
|
||||||
import AppUpdater from './services/AppUpdater'
|
import AppUpdater from './services/AppUpdater'
|
||||||
import BackupManager from './services/BackupManager'
|
import BackupManager from './services/BackupManager'
|
||||||
|
import { CacheService } from './services/CacheService'
|
||||||
import { configManager } from './services/ConfigManager'
|
import { configManager } from './services/ConfigManager'
|
||||||
import CopilotService from './services/CopilotService'
|
import CopilotService from './services/CopilotService'
|
||||||
import { ExportService } from './services/ExportService'
|
import { ExportService } from './services/ExportService'
|
||||||
@@ -30,6 +31,7 @@ import { pythonService } from './services/PythonService'
|
|||||||
import { FileServiceManager } from './services/remotefile/FileServiceManager'
|
import { FileServiceManager } from './services/remotefile/FileServiceManager'
|
||||||
import { searchService } from './services/SearchService'
|
import { searchService } from './services/SearchService'
|
||||||
import { SelectionService } from './services/SelectionService'
|
import { SelectionService } from './services/SelectionService'
|
||||||
|
import { SettingsWindowService } from './services/SettingsWindowService'
|
||||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||||
import storeSyncService from './services/StoreSyncService'
|
import storeSyncService from './services/StoreSyncService'
|
||||||
import { themeService } from './services/ThemeService'
|
import { themeService } from './services/ThemeService'
|
||||||
@@ -577,4 +579,12 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.App_SetDisableHardwareAcceleration, (_, isDisable: boolean) => {
|
ipcMain.handle(IpcChannel.App_SetDisableHardwareAcceleration, (_, isDisable: boolean) => {
|
||||||
configManager.setDisableHardwareAcceleration(isDisable)
|
configManager.setDisableHardwareAcceleration(isDisable)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
ipcMain.handle(IpcChannel.Navigation_Url, (_, url: string) => {
|
||||||
|
CacheService.set('navigation-url', url)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Settings Window
|
||||||
|
SettingsWindowService.registerIpcHandler()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
interface CacheItem<T> {
|
interface CacheItem<T> {
|
||||||
data: T
|
data: T
|
||||||
timestamp: number
|
timestamp: number
|
||||||
duration: number
|
duration?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CacheService {
|
export class CacheService {
|
||||||
@@ -11,9 +11,9 @@ export class CacheService {
|
|||||||
* Set cache
|
* Set cache
|
||||||
* @param key Cache key
|
* @param key Cache key
|
||||||
* @param data Cache data
|
* @param data Cache data
|
||||||
* @param duration Cache duration (in milliseconds)
|
* @param duration Cache duration (in milliseconds), if not set the cache will never expire
|
||||||
*/
|
*/
|
||||||
static set<T>(key: string, data: T, duration: number): void {
|
static set<T>(key: string, data: T, duration?: number): void {
|
||||||
this.cache.set(key, {
|
this.cache.set(key, {
|
||||||
data,
|
data,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -30,6 +30,11 @@ export class CacheService {
|
|||||||
const item = this.cache.get(key)
|
const item = this.cache.get(key)
|
||||||
if (!item) return null
|
if (!item) return null
|
||||||
|
|
||||||
|
// If duration is undefined, cache never expires
|
||||||
|
if (item.duration === undefined) {
|
||||||
|
return item.data
|
||||||
|
}
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (now - item.timestamp > item.duration) {
|
if (now - item.timestamp > item.duration) {
|
||||||
this.remove(key)
|
this.remove(key)
|
||||||
@@ -63,6 +68,11 @@ export class CacheService {
|
|||||||
const item = this.cache.get(key)
|
const item = this.cache.get(key)
|
||||||
if (!item) return false
|
if (!item) return false
|
||||||
|
|
||||||
|
// If duration is undefined, cache never expires
|
||||||
|
if (item.duration === undefined) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
if (now - item.timestamp > item.duration) {
|
if (now - item.timestamp > item.duration) {
|
||||||
this.remove(key)
|
this.remove(key)
|
||||||
|
|||||||
149
src/main/services/SettingsWindowService.ts
Normal file
149
src/main/services/SettingsWindowService.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { is } from '@electron-toolkit/utils'
|
||||||
|
import { isLinux, isMac } from '@main/constant'
|
||||||
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
|
import { BrowserWindow, nativeTheme } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
|
||||||
|
import icon from '../../../build/icon.png?asset'
|
||||||
|
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
|
||||||
|
|
||||||
|
export class SettingsWindowService {
|
||||||
|
private static instance: SettingsWindowService | null = null
|
||||||
|
private settingsWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
|
public static getInstance(): SettingsWindowService {
|
||||||
|
if (!SettingsWindowService.instance) {
|
||||||
|
SettingsWindowService.instance = new SettingsWindowService()
|
||||||
|
}
|
||||||
|
return SettingsWindowService.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
public createSettingsWindow(defaultTab?: string): BrowserWindow {
|
||||||
|
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
|
||||||
|
this.settingsWindow.show()
|
||||||
|
this.settingsWindow.focus()
|
||||||
|
return this.settingsWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settingsWindow = new BrowserWindow({
|
||||||
|
width: 1000,
|
||||||
|
height: 700,
|
||||||
|
minWidth: 900,
|
||||||
|
minHeight: 600,
|
||||||
|
show: false,
|
||||||
|
autoHideMenuBar: true,
|
||||||
|
transparent: false,
|
||||||
|
vibrancy: 'sidebar',
|
||||||
|
visualEffectState: 'active',
|
||||||
|
titleBarStyle: 'hidden',
|
||||||
|
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
|
||||||
|
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
|
||||||
|
darkTheme: nativeTheme.shouldUseDarkColors,
|
||||||
|
trafficLightPosition: { x: 12, y: 12 },
|
||||||
|
...(isLinux ? { icon } : {}),
|
||||||
|
webPreferences: {
|
||||||
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
|
sandbox: false,
|
||||||
|
webSecurity: false,
|
||||||
|
webviewTag: true,
|
||||||
|
allowRunningInsecureContent: true,
|
||||||
|
backgroundThrottling: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.setupSettingsWindow()
|
||||||
|
this.loadSettingsWindowContent(defaultTab)
|
||||||
|
|
||||||
|
return this.settingsWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSettingsWindow() {
|
||||||
|
if (!this.settingsWindow) return
|
||||||
|
|
||||||
|
this.settingsWindow.on('ready-to-show', () => {
|
||||||
|
this.settingsWindow?.show()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.settingsWindow.on('closed', () => {
|
||||||
|
this.settingsWindow = null
|
||||||
|
})
|
||||||
|
|
||||||
|
this.settingsWindow.on('close', () => {
|
||||||
|
// Clean up when window is closed
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle theme changes
|
||||||
|
nativeTheme.on('updated', () => {
|
||||||
|
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
|
||||||
|
this.settingsWindow.setTitleBarOverlay(
|
||||||
|
nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadSettingsWindowContent(defaultTab?: string) {
|
||||||
|
if (!this.settingsWindow) return
|
||||||
|
|
||||||
|
const queryParam = defaultTab ? `?tab=${defaultTab}` : ''
|
||||||
|
|
||||||
|
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||||
|
this.settingsWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + '/settingsWindow.html' + queryParam)
|
||||||
|
} else {
|
||||||
|
this.settingsWindow.loadFile(join(__dirname, '../renderer/settingsWindow.html'))
|
||||||
|
if (defaultTab) {
|
||||||
|
this.settingsWindow.webContents.once('did-finish-load', () => {
|
||||||
|
this.settingsWindow?.webContents.send(IpcChannel.SettingsWindow_Show, { defaultTab })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public showSettingsWindow(defaultTab?: string) {
|
||||||
|
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
|
||||||
|
if (this.settingsWindow.isMinimized()) {
|
||||||
|
this.settingsWindow.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLinux) {
|
||||||
|
this.settingsWindow.setVisibleOnAllWorkspaces(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.settingsWindow.show()
|
||||||
|
this.settingsWindow.focus()
|
||||||
|
|
||||||
|
if (!isLinux) {
|
||||||
|
this.settingsWindow.setVisibleOnAllWorkspaces(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.createSettingsWindow(defaultTab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public hideSettingsWindow() {
|
||||||
|
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
|
||||||
|
this.settingsWindow.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeSettingsWindow() {
|
||||||
|
if (this.settingsWindow && !this.settingsWindow.isDestroyed()) {
|
||||||
|
this.settingsWindow.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSettingsWindow(): BrowserWindow | null {
|
||||||
|
return this.settingsWindow
|
||||||
|
}
|
||||||
|
|
||||||
|
public static registerIpcHandler() {
|
||||||
|
const { ipcMain } = require('electron')
|
||||||
|
const service = SettingsWindowService.getInstance()
|
||||||
|
|
||||||
|
ipcMain.handle(IpcChannel.SettingsWindow_Show, (_, options?: { defaultTab?: string }) => {
|
||||||
|
service.showSettingsWindow(options?.defaultTab)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsWindowService = SettingsWindowService.getInstance()
|
||||||
@@ -5,10 +5,12 @@ import Logger from 'electron-log'
|
|||||||
|
|
||||||
import { configManager } from './ConfigManager'
|
import { configManager } from './ConfigManager'
|
||||||
import selectionService from './SelectionService'
|
import selectionService from './SelectionService'
|
||||||
|
import { settingsWindowService } from './SettingsWindowService'
|
||||||
import { windowService } from './WindowService'
|
import { windowService } from './WindowService'
|
||||||
|
|
||||||
let showAppAccelerator: string | null = null
|
let showAppAccelerator: string | null = null
|
||||||
let showMiniWindowAccelerator: string | null = null
|
let showMiniWindowAccelerator: string | null = null
|
||||||
|
let showSettingsAccelerator: string | null = null
|
||||||
let selectionAssistantToggleAccelerator: string | null = null
|
let selectionAssistantToggleAccelerator: string | null = null
|
||||||
let selectionAssistantSelectTextAccelerator: string | null = null
|
let selectionAssistantSelectTextAccelerator: string | null = null
|
||||||
|
|
||||||
@@ -26,6 +28,10 @@ function getShortcutHandler(shortcut: Shortcut) {
|
|||||||
return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
|
return (window: BrowserWindow) => handleZoomFactor([window], -0.1)
|
||||||
case 'zoom_reset':
|
case 'zoom_reset':
|
||||||
return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
|
return (window: BrowserWindow) => handleZoomFactor([window], 0, true)
|
||||||
|
case 'show_settings':
|
||||||
|
return () => {
|
||||||
|
settingsWindowService.showSettingsWindow()
|
||||||
|
}
|
||||||
case 'show_app':
|
case 'show_app':
|
||||||
return () => {
|
return () => {
|
||||||
windowService.toggleMainWindow()
|
windowService.toggleMainWindow()
|
||||||
@@ -146,9 +152,13 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
// only register universal shortcuts when needed
|
// only register universal shortcuts when needed
|
||||||
if (
|
if (
|
||||||
onlyUniversalShortcuts &&
|
onlyUniversalShortcuts &&
|
||||||
!['show_app', 'mini_window', 'selection_assistant_toggle', 'selection_assistant_select_text'].includes(
|
![
|
||||||
shortcut.key
|
'show_app',
|
||||||
)
|
'mini_window',
|
||||||
|
'show_settings',
|
||||||
|
'selection_assistant_toggle',
|
||||||
|
'selection_assistant_select_text'
|
||||||
|
].includes(shortcut.key)
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -171,6 +181,10 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
|
showMiniWindowAccelerator = formatShortcutKey(shortcut.shortcut)
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'show_settings':
|
||||||
|
showSettingsAccelerator = formatShortcutKey(shortcut.shortcut)
|
||||||
|
break
|
||||||
|
|
||||||
case 'selection_assistant_toggle':
|
case 'selection_assistant_toggle':
|
||||||
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
|
selectionAssistantToggleAccelerator = formatShortcutKey(shortcut.shortcut)
|
||||||
break
|
break
|
||||||
@@ -222,6 +236,12 @@ export function registerShortcuts(window: BrowserWindow) {
|
|||||||
handler && globalShortcut.register(accelerator, () => handler(window))
|
handler && globalShortcut.register(accelerator, () => handler(window))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showSettingsAccelerator) {
|
||||||
|
const handler = getShortcutHandler({ key: 'show_settings' } as Shortcut)
|
||||||
|
const accelerator = convertShortcutFormat(showSettingsAccelerator)
|
||||||
|
handler && globalShortcut.register(accelerator, () => handler(window))
|
||||||
|
}
|
||||||
|
|
||||||
if (selectionAssistantToggleAccelerator) {
|
if (selectionAssistantToggleAccelerator) {
|
||||||
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
|
const handler = getShortcutHandler({ key: 'selection_assistant_toggle' } as Shortcut)
|
||||||
const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
|
const accelerator = convertShortcutFormat(selectionAssistantToggleAccelerator)
|
||||||
@@ -258,6 +278,7 @@ export function unregisterAllShortcuts() {
|
|||||||
try {
|
try {
|
||||||
showAppAccelerator = null
|
showAppAccelerator = null
|
||||||
showMiniWindowAccelerator = null
|
showMiniWindowAccelerator = null
|
||||||
|
showSettingsAccelerator = null
|
||||||
selectionAssistantToggleAccelerator = null
|
selectionAssistantToggleAccelerator = null
|
||||||
selectionAssistantSelectTextAccelerator = null
|
selectionAssistantSelectTextAccelerator = null
|
||||||
windowOnHandlers.forEach((handlers, window) => {
|
windowOnHandlers.forEach((handlers, window) => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { join } from 'path'
|
|||||||
|
|
||||||
import icon from '../../../build/icon.png?asset'
|
import icon from '../../../build/icon.png?asset'
|
||||||
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
|
import { titleBarOverlayDark, titleBarOverlayLight } from '../config'
|
||||||
|
import { CacheService } from './CacheService'
|
||||||
import { configManager } from './ConfigManager'
|
import { configManager } from './ConfigManager'
|
||||||
import { contextMenu } from './ContextMenu'
|
import { contextMenu } from './ContextMenu'
|
||||||
import { initSessionUserAgent } from './WebviewService'
|
import { initSessionUserAgent } from './WebviewService'
|
||||||
@@ -63,7 +64,7 @@ export class WindowService {
|
|||||||
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
|
titleBarOverlay: nativeTheme.shouldUseDarkColors ? titleBarOverlayDark : titleBarOverlayLight,
|
||||||
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
|
backgroundColor: isMac ? undefined : nativeTheme.shouldUseDarkColors ? '#181818' : '#FFFFFF',
|
||||||
darkTheme: nativeTheme.shouldUseDarkColors,
|
darkTheme: nativeTheme.shouldUseDarkColors,
|
||||||
trafficLightPosition: { x: 8, y: 12 },
|
trafficLightPosition: { x: 12, y: 12 },
|
||||||
...(isLinux ? { icon } : {}),
|
...(isLinux ? { icon } : {}),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.js'),
|
preload: join(__dirname, '../preload/index.js'),
|
||||||
@@ -313,6 +314,11 @@ export class WindowService {
|
|||||||
return app.quit()
|
return app.quit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CacheService.get('navigation-url') !== '/') {
|
||||||
|
event.preventDefault()
|
||||||
|
return mainWindow.webContents.send(IpcChannel.Navigation_Close)
|
||||||
|
}
|
||||||
|
|
||||||
// 托盘及关闭行为设置
|
// 托盘及关闭行为设置
|
||||||
const isShowTray = configManager.getTray()
|
const isShowTray = configManager.getTray()
|
||||||
const isTrayOnClose = configManager.getTrayOnClose()
|
const isTrayOnClose = configManager.getTrayOnClose()
|
||||||
@@ -435,8 +441,9 @@ export class WindowService {
|
|||||||
show: false,
|
show: false,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
transparent: isMac,
|
transparent: isMac,
|
||||||
vibrancy: 'under-window',
|
vibrancy: isMac ? 'under-window' : undefined,
|
||||||
visualEffectState: 'followWindow',
|
visualEffectState: isMac ? 'followWindow' : undefined,
|
||||||
|
backgroundMaterial: isWin ? 'acrylic' : undefined,
|
||||||
center: true,
|
center: true,
|
||||||
frame: false,
|
frame: false,
|
||||||
alwaysOnTop: true,
|
alwaysOnTop: true,
|
||||||
|
|||||||
@@ -318,8 +318,25 @@ const api = {
|
|||||||
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
|
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
|
||||||
},
|
},
|
||||||
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text),
|
quoteToMainWindow: (text: string) => ipcRenderer.invoke(IpcChannel.App_QuoteToMain, text),
|
||||||
|
navigation: {
|
||||||
|
url: (url: string) => ipcRenderer.invoke(IpcChannel.Navigation_Url, url)
|
||||||
|
},
|
||||||
setDisableHardwareAcceleration: (isDisable: boolean) =>
|
setDisableHardwareAcceleration: (isDisable: boolean) =>
|
||||||
ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable)
|
ipcRenderer.invoke(IpcChannel.App_SetDisableHardwareAcceleration, isDisable),
|
||||||
|
|
||||||
|
// Settings Window
|
||||||
|
showSettingsWindow: (options?: { defaultTab?: string }) =>
|
||||||
|
ipcRenderer.invoke(IpcChannel.SettingsWindow_Show, options),
|
||||||
|
|
||||||
|
on: (channel: string, func: any) => {
|
||||||
|
const listener = (_event: Electron.IpcRendererEvent, ...args: any[]) => {
|
||||||
|
func(...args)
|
||||||
|
}
|
||||||
|
ipcRenderer.on(channel, listener)
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.off(channel, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `contextBridge` APIs to expose Electron APIs to
|
// Use `contextBridge` APIs to expose Electron APIs to
|
||||||
|
|||||||
23
src/renderer/settingsWindow.html
Normal file
23
src/renderer/settingsWindow.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="initial-scale=1, width=device-width" />
|
||||||
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self'; connect-src blob: *; script-src 'self' 'unsafe-eval' *; worker-src 'self' blob:; style-src 'self' 'unsafe-inline' *; font-src 'self' data: *; img-src 'self' data: file: * blob:; frame-src * file:" />
|
||||||
|
<title>Cherry Studio</title>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/windows/settings/entryPoint.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,10 +2,10 @@ import '@renderer/databases'
|
|||||||
|
|
||||||
import store, { persistor } from '@renderer/store'
|
import store, { persistor } from '@renderer/store'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { HashRouter, Route, Routes } from 'react-router-dom'
|
import { HashRouter } from 'react-router-dom'
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
import { PersistGate } from 'redux-persist/integration/react'
|
||||||
|
|
||||||
import Sidebar from './components/app/Sidebar'
|
import AppLayout from './components/Layout/AppLayout'
|
||||||
import TopViewContainer from './components/TopView'
|
import TopViewContainer from './components/TopView'
|
||||||
import AntdProvider from './context/AntdProvider'
|
import AntdProvider from './context/AntdProvider'
|
||||||
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
import { CodeStyleProvider } from './context/CodeStyleProvider'
|
||||||
@@ -13,14 +13,8 @@ import { NotificationProvider } from './context/NotificationProvider'
|
|||||||
import StyleSheetManager from './context/StyleSheetManager'
|
import StyleSheetManager from './context/StyleSheetManager'
|
||||||
import { ThemeProvider } from './context/ThemeProvider'
|
import { ThemeProvider } from './context/ThemeProvider'
|
||||||
import NavigationHandler from './handler/NavigationHandler'
|
import NavigationHandler from './handler/NavigationHandler'
|
||||||
import AgentsPage from './pages/agents/AgentsPage'
|
import { ChatProvider } from './hooks/useChat'
|
||||||
import AppsPage from './pages/apps/AppsPage'
|
import Routes from './Routes'
|
||||||
import FilesPage from './pages/files/FilesPage'
|
|
||||||
import HomePage from './pages/home/HomePage'
|
|
||||||
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
|
||||||
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
|
||||||
import SettingsPage from './pages/settings/SettingsPage'
|
|
||||||
import TranslatePage from './pages/translate/TranslatePage'
|
|
||||||
|
|
||||||
function App(): React.ReactElement {
|
function App(): React.ReactElement {
|
||||||
return (
|
return (
|
||||||
@@ -31,22 +25,16 @@ function App(): React.ReactElement {
|
|||||||
<NotificationProvider>
|
<NotificationProvider>
|
||||||
<CodeStyleProvider>
|
<CodeStyleProvider>
|
||||||
<PersistGate loading={null} persistor={persistor}>
|
<PersistGate loading={null} persistor={persistor}>
|
||||||
<TopViewContainer>
|
<HashRouter>
|
||||||
<HashRouter>
|
<TopViewContainer>
|
||||||
<NavigationHandler />
|
<NavigationHandler />
|
||||||
<Sidebar />
|
<ChatProvider>
|
||||||
<Routes>
|
<AppLayout>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Routes />
|
||||||
<Route path="/agents" element={<AgentsPage />} />
|
</AppLayout>
|
||||||
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
|
</ChatProvider>
|
||||||
<Route path="/translate" element={<TranslatePage />} />
|
</TopViewContainer>
|
||||||
<Route path="/files" element={<FilesPage />} />
|
</HashRouter>
|
||||||
<Route path="/knowledge" element={<KnowledgePage />} />
|
|
||||||
<Route path="/apps" element={<AppsPage />} />
|
|
||||||
<Route path="/settings/*" element={<SettingsPage />} />
|
|
||||||
</Routes>
|
|
||||||
</HashRouter>
|
|
||||||
</TopViewContainer>
|
|
||||||
</PersistGate>
|
</PersistGate>
|
||||||
</CodeStyleProvider>
|
</CodeStyleProvider>
|
||||||
</NotificationProvider>
|
</NotificationProvider>
|
||||||
|
|||||||
29
src/renderer/src/Routes.tsx
Normal file
29
src/renderer/src/Routes.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Route, Routes } from 'react-router-dom'
|
||||||
|
|
||||||
|
import AgentsPage from './pages/agents/AgentsPage'
|
||||||
|
import AppsPage from './pages/apps/AppsPage'
|
||||||
|
import FilesPage from './pages/files/FilesPage'
|
||||||
|
import HomePage from './pages/home/HomePage'
|
||||||
|
import KnowledgePage from './pages/knowledge/KnowledgePage'
|
||||||
|
import McpServersPage from './pages/mcp-servers'
|
||||||
|
import PaintingsRoutePage from './pages/paintings/PaintingsRoutePage'
|
||||||
|
import TranslatePage from './pages/translate/TranslatePage'
|
||||||
|
|
||||||
|
const RouteContainer = () => {
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/agents" element={<AgentsPage />} />
|
||||||
|
<Route path="/paintings/*" element={<PaintingsRoutePage />} />
|
||||||
|
<Route path="/translate" element={<TranslatePage />} />
|
||||||
|
<Route path="/files" element={<FilesPage />} />
|
||||||
|
<Route path="/knowledge" element={<KnowledgePage />} />
|
||||||
|
<Route path="/apps" element={<AppsPage />} />
|
||||||
|
<Route path="/mcp-servers/*" element={<McpServersPage />} />
|
||||||
|
{/* <Route path="/settings/*" element={<SettingsPage />} />
|
||||||
|
<Route path="/launchpad" element={<LaunchpadPage />} /> */}
|
||||||
|
</Routes>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RouteContainer
|
||||||
@@ -25,7 +25,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.minapp-drawer {
|
.minapp-drawer {
|
||||||
max-width: calc(100vw - var(--sidebar-width));
|
|
||||||
.ant-drawer-content-wrapper {
|
.ant-drawer-content-wrapper {
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
@@ -33,7 +32,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
min-height: calc(var(--navbar-height) + 0.5px);
|
min-height: calc(var(--navbar-height) + 0.5px);
|
||||||
width: calc(100vw - var(--sidebar-width));
|
width: 100%;
|
||||||
margin-top: -0.5px;
|
margin-top: -0.5px;
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
--color-text-secondary: rgba(235, 235, 245, 0.7);
|
--color-text-secondary: rgba(235, 235, 245, 0.7);
|
||||||
--color-icon: #ffffff99;
|
--color-icon: #ffffff99;
|
||||||
--color-icon-white: #ffffff;
|
--color-icon-white: #ffffff;
|
||||||
--color-border: #ffffff19;
|
--color-border: #383838;
|
||||||
--color-border-soft: #ffffff10;
|
--color-border-soft: #ffffff10;
|
||||||
--color-border-mute: #ffffff05;
|
--color-border-mute: #ffffff05;
|
||||||
--color-error: #f44336;
|
--color-error: #f44336;
|
||||||
@@ -54,25 +54,14 @@
|
|||||||
--color-background-highlight-accent: rgba(255, 150, 50, 0.9);
|
--color-background-highlight-accent: rgba(255, 150, 50, 0.9);
|
||||||
|
|
||||||
--navbar-background-mac: rgba(20, 20, 20, 0.55);
|
--navbar-background-mac: rgba(20, 20, 20, 0.55);
|
||||||
|
--navbar-background-win: rgba(20, 20, 20, 0.75);
|
||||||
--navbar-background: #1f1f1f;
|
--navbar-background: #1f1f1f;
|
||||||
|
|
||||||
--navbar-height: 40px;
|
|
||||||
--sidebar-width: 50px;
|
|
||||||
--status-bar-height: 40px;
|
|
||||||
--input-bar-height: 100px;
|
|
||||||
|
|
||||||
--assistants-width: 275px;
|
|
||||||
--topic-list-width: 275px;
|
|
||||||
--settings-width: 250px;
|
|
||||||
--scrollbar-width: 5px;
|
|
||||||
|
|
||||||
--chat-background: transparent;
|
--chat-background: transparent;
|
||||||
--chat-background-user: rgba(255, 255, 255, 0.08);
|
--chat-background-user: rgba(255, 255, 255, 0.08);
|
||||||
--chat-background-assistant: transparent;
|
--chat-background-assistant: transparent;
|
||||||
--chat-text-user: var(--color-black);
|
--chat-text-user: var(--color-black);
|
||||||
|
|
||||||
--list-item-border-radius: 20px;
|
|
||||||
|
|
||||||
--color-status-success: #52c41a;
|
--color-status-success: #52c41a;
|
||||||
--color-status-error: #ff4d4f;
|
--color-status-error: #ff4d4f;
|
||||||
--color-status-warning: #faad14;
|
--color-status-warning: #faad14;
|
||||||
@@ -124,8 +113,8 @@
|
|||||||
--color-reference-text: #000000;
|
--color-reference-text: #000000;
|
||||||
--color-reference-background: #f1f7ff;
|
--color-reference-background: #f1f7ff;
|
||||||
|
|
||||||
--color-list-item: #eee;
|
--color-list-item: rgba(0, 0, 0, 0.05);
|
||||||
--color-list-item-hover: #f5f5f5;
|
--color-list-item-hover: rgba(0, 0, 0, 0.03);
|
||||||
|
|
||||||
--modal-background: var(--color-white);
|
--modal-background: var(--color-white);
|
||||||
|
|
||||||
@@ -134,6 +123,7 @@
|
|||||||
--color-background-highlight-accent: rgba(255, 150, 50, 0.5);
|
--color-background-highlight-accent: rgba(255, 150, 50, 0.5);
|
||||||
|
|
||||||
--navbar-background-mac: rgba(255, 255, 255, 0.55);
|
--navbar-background-mac: rgba(255, 255, 255, 0.55);
|
||||||
|
--navbar-background-win: rgba(255, 255, 255, 0.75);
|
||||||
--navbar-background: rgba(244, 244, 244);
|
--navbar-background: rgba(244, 244, 244);
|
||||||
|
|
||||||
--chat-background: transparent;
|
--chat-background: transparent;
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
#content-container {
|
#content-container {
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
border-top: 0.5px solid var(--color-border);
|
border-top: 0.5px solid var(--color-border);
|
||||||
border-top-left-radius: 10px;
|
}
|
||||||
border-left: 0.5px solid var(--color-border);
|
|
||||||
|
.group-container {
|
||||||
|
.context-menu-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-menu-container {
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
@use './variables.scss';
|
||||||
@use './color.scss';
|
@use './color.scss';
|
||||||
@use './font.scss';
|
@use './font.scss';
|
||||||
@use './markdown.scss';
|
@use './markdown.scss';
|
||||||
|
|||||||
18
src/renderer/src/assets/styles/variables.scss
Normal file
18
src/renderer/src/assets/styles/variables.scss
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
:root {
|
||||||
|
--navbar-height: 42px;
|
||||||
|
--sidebar-width: 50px;
|
||||||
|
--status-bar-height: 40px;
|
||||||
|
--input-bar-height: 100px;
|
||||||
|
|
||||||
|
--assistants-width: 275px;
|
||||||
|
--topic-list-width: 275px;
|
||||||
|
--settings-width: 250px;
|
||||||
|
--scrollbar-width: 5px;
|
||||||
|
|
||||||
|
--list-item-border-radius: 15px;
|
||||||
|
--border-width: 0.5px;
|
||||||
|
|
||||||
|
--main-height: 100vh;
|
||||||
|
|
||||||
|
--border-width: 0.5px;
|
||||||
|
}
|
||||||
@@ -292,6 +292,10 @@ const SplitViewWrapper = styled.div`
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(:has(+ .html-artifacts)) {
|
||||||
|
border-radius: 0 0 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(:has(+ [class*='Container'])) {
|
&:not(:has(+ [class*='Container'])) {
|
||||||
border-radius: 0 0 8px 8px;
|
border-radius: 0 0 8px 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import {
|
|||||||
Text as UnWrapIcon,
|
Text as UnWrapIcon,
|
||||||
WrapText as WrapIcon
|
WrapText as WrapIcon
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { memo } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
import { useBlurHandler, useLanguageExtensions, useSaveKeymap } from './hooks'
|
import { useBlurHandler, useLanguageExtensions, useSaveKeymap } from './hooks'
|
||||||
|
|||||||
@@ -348,20 +348,23 @@ export const ContentSearch = React.forwardRef<ContentSearchRef, Props>(
|
|||||||
<ToolBar>
|
<ToolBar>
|
||||||
<Tooltip title={t('button.includes_user_questions')} mouseEnterDelay={0.8} placement="bottom">
|
<Tooltip title={t('button.includes_user_questions')} mouseEnterDelay={0.8} placement="bottom">
|
||||||
<ToolbarButton type="text" onClick={userOutlinedButtonOnClick}>
|
<ToolbarButton type="text" onClick={userOutlinedButtonOnClick}>
|
||||||
<User size={18} style={{ color: includeUser ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
<User size={18} style={{ color: includeUser ? 'var(--color-primary)' : 'var(--color-icon)' }} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('button.case_sensitive')} mouseEnterDelay={0.8} placement="bottom">
|
<Tooltip title={t('button.case_sensitive')} mouseEnterDelay={0.8} placement="bottom">
|
||||||
<ToolbarButton type="text" onClick={caseSensitiveButtonOnClick}>
|
<ToolbarButton type="text" onClick={caseSensitiveButtonOnClick}>
|
||||||
<CaseSensitive
|
<CaseSensitive
|
||||||
size={18}
|
size={18}
|
||||||
style={{ color: isCaseSensitive ? 'var(--color-link)' : 'var(--color-icon)' }}
|
style={{ color: isCaseSensitive ? 'var(--color-primary)' : 'var(--color-icon)' }}
|
||||||
/>
|
/>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title={t('button.whole_word')} mouseEnterDelay={0.8} placement="bottom">
|
<Tooltip title={t('button.whole_word')} mouseEnterDelay={0.8} placement="bottom">
|
||||||
<ToolbarButton type="text" onClick={wholeWordButtonOnClick}>
|
<ToolbarButton type="text" onClick={wholeWordButtonOnClick}>
|
||||||
<WholeWord size={18} style={{ color: isWholeWord ? 'var(--color-link)' : 'var(--color-icon)' }} />
|
<WholeWord
|
||||||
|
size={18}
|
||||||
|
style={{ color: isWholeWord ? 'var(--color-primary)' : 'var(--color-icon)' }}
|
||||||
|
/>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</ToolBar>
|
</ToolBar>
|
||||||
@@ -406,7 +409,6 @@ const Container = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const SearchBarContainer = styled.div`
|
const SearchBarContainer = styled.div`
|
||||||
border: 1px solid var(--color-primary);
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -420,6 +422,7 @@ const SearchBarContainer = styled.div`
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--color-background);
|
background-color: var(--color-background);
|
||||||
flex: 1 1 auto; /* Take up input's previous space */
|
flex: 1 1 auto; /* Take up input's previous space */
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
`
|
`
|
||||||
|
|
||||||
const Placeholder = styled.div`
|
const Placeholder = styled.div`
|
||||||
|
|||||||
17
src/renderer/src/components/CustomSelect.tsx
Normal file
17
src/renderer/src/components/CustomSelect.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { includeKeywords } from '@renderer/utils/search'
|
||||||
|
import { Select, SelectProps } from 'antd'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义 Select,使用增强的搜索 filter
|
||||||
|
*/
|
||||||
|
const CustomSelect = ({ ref, ...props }: SelectProps & { ref?: React.RefObject<any | null> }) => {
|
||||||
|
return <Select ref={ref} filterOption={enhancedFilterOption} {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomSelect.displayName = 'CustomSelect'
|
||||||
|
|
||||||
|
function enhancedFilterOption(input: string, option: any) {
|
||||||
|
return includeKeywords(option.label, input)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomSelect
|
||||||
@@ -51,23 +51,27 @@ const DraggableList: FC<Props<any>> = ({
|
|||||||
<VirtualList data={list} itemKey="id">
|
<VirtualList data={list} itemKey="id">
|
||||||
{(item, index) => {
|
{(item, index) => {
|
||||||
const id = item.id || item
|
const id = item.id || item
|
||||||
return (
|
if (!item.disabled) {
|
||||||
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
|
return (
|
||||||
{(provided) => (
|
<Draggable key={`draggable_${id}_${index}`} draggableId={id} index={index}>
|
||||||
<div
|
{(provided) => (
|
||||||
ref={provided.innerRef}
|
<div
|
||||||
{...provided.draggableProps}
|
ref={provided.innerRef}
|
||||||
{...provided.dragHandleProps}
|
{...provided.draggableProps}
|
||||||
style={{
|
{...provided.dragHandleProps}
|
||||||
...listStyle,
|
style={{
|
||||||
...provided.draggableProps.style,
|
marginBottom: 8,
|
||||||
marginBottom: 8
|
...listStyle,
|
||||||
}}>
|
...provided.draggableProps.style
|
||||||
{children(item, index)}
|
}}>
|
||||||
</div>
|
{children(item, index)}
|
||||||
)}
|
</div>
|
||||||
</Draggable>
|
)}
|
||||||
)
|
</Draggable>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return <div> {children(item, index)}</div>
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
</VirtualList>
|
</VirtualList>
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
|
|||||||
35
src/renderer/src/components/Icons/NarrowModeIcon.tsx
Normal file
35
src/renderer/src/components/Icons/NarrowModeIcon.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isNarrowMode: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const NarrowModeIcon: FC<Props> = ({ isNarrowMode }) => {
|
||||||
|
return (
|
||||||
|
<Container $isNarrowMode={isNarrowMode}>
|
||||||
|
<Line />
|
||||||
|
<Line />
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div<{ $isNarrowMode: boolean }>`
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 1.5px solid var(--color-text-2);
|
||||||
|
border-radius: 3px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: ${({ $isNarrowMode }) => ($isNarrowMode ? 'space-evenly' : 'space-between')};
|
||||||
|
padding: 2px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Line = styled.div`
|
||||||
|
width: 2px;
|
||||||
|
height: 10px;
|
||||||
|
background-color: var(--color-text-2);
|
||||||
|
border-radius: 5px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default NarrowModeIcon
|
||||||
42
src/renderer/src/components/Icons/PanelIcons.tsx
Normal file
42
src/renderer/src/components/Icons/PanelIcons.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { SVGProps } from 'react'
|
||||||
|
|
||||||
|
interface PanelIconProps extends Omit<SVGProps<SVGSVGElement>, 'width' | 'height'> {
|
||||||
|
size?: number | string
|
||||||
|
expanded?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelLeftIcon = ({ size = 18, expanded = false, ...props }: PanelIconProps) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
className="lucide lucide-panel-left-icon lucide-panel-left"
|
||||||
|
{...props}>
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
{expanded ? <path d="M10 7v10" strokeWidth={4} /> : <path d="M9 6v12" strokeWidth={2} />}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const PanelRightIcon = ({ size = 18, expanded = false, ...props }: PanelIconProps) => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
className="lucide lucide-panel-right-icon lucide-panel-right"
|
||||||
|
{...props}>
|
||||||
|
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||||
|
{expanded ? <path d="M14 7v10" strokeWidth={4} /> : <path d="M15 6v12" strokeWidth={2} />}
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
@@ -22,7 +22,7 @@ const Container = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const Icon = styled.i`
|
const Icon = styled.i`
|
||||||
color: var(--color-link);
|
color: var(--color-primary);
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -77,3 +77,18 @@ export function MdiLightbulbOn90(props: SVGProps<SVGSVGElement>) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ExpandWidth(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg width="1em" height="1em" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
id="path"
|
||||||
|
d="M0 25L0 175C0 179.41 3.58 183 8 183L12 183C16.41 183 20 179.41 20 175L20 25C20 20.58 16.41 17 12 17L8 17C3.58 17 0 20.58 0 25ZM60.41 58.43L29.42 94.81C26.87 97.8 26.87 102.19 29.42 105.18L60.38 141.53C65.2 147.19 74.47 143.78 74.47 136.34L74.47 121.83C74.47 117.41 78.06 113.83 82.47 113.83L117.5 113.83C121.91 113.83 125.5 117.41 125.5 121.83L125.5 136.35C125.5 143.78 134.76 147.2 139.58 141.54L170.57 105.18C173.12 102.19 173.12 97.8 170.57 94.81L139.59 58.43C134.76 52.77 125.5 56.18 125.5 63.62L125.5 78.16C125.5 82.58 121.91 86.16 117.5 86.16L82.5 86.16C78.08 86.16 74.5 82.58 74.5 78.16L74.5 63.62C74.5 56.18 65.23 52.77 60.41 58.43ZM188 17L192 17C196.41 17 200 20.58 200 25L200 175C200 179.41 196.41 183 192 183L188 183C183.58 183 180 179.41 180 175L180 25C180 20.58 183.58 17 188 17Z"
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="nonzero"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const Container = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const Icon = styled(GlobalOutlined)`
|
const Icon = styled(GlobalOutlined)`
|
||||||
color: var(--color-link);
|
color: var(--color-primary);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
`
|
`
|
||||||
|
|||||||
27
src/renderer/src/components/Layout/AppLayout.tsx
Normal file
27
src/renderer/src/components/Layout/AppLayout.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import MainSidebar from '@renderer/pages/home/MainSidebar/MainSidebar'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface AppLayoutProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const AppLayout: FC<AppLayoutProps> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<HStack style={{ display: 'flex', flex: 1 }} id="app-layout">
|
||||||
|
<MainSidebar />
|
||||||
|
<ContentArea>{children}</ContentArea>
|
||||||
|
</HStack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ContentArea = styled.div`
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default AppLayout
|
||||||
@@ -395,10 +395,7 @@ const MinappPopupContainer: React.FC = () => {
|
|||||||
height={'100%'}
|
height={'100%'}
|
||||||
maskClosable={false}
|
maskClosable={false}
|
||||||
closeIcon={null}
|
closeIcon={null}
|
||||||
style={{
|
style={{ backgroundColor: window.root.style.background }}>
|
||||||
marginLeft: 'var(--sidebar-width)',
|
|
||||||
backgroundColor: window.root.style.background
|
|
||||||
}}>
|
|
||||||
{!isReady && (
|
{!isReady && (
|
||||||
<EmptyView>
|
<EmptyView>
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -418,7 +415,7 @@ const TitleContainer = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: ${isMac ? '20px' : '10px'};
|
padding-left: ${isMac ? '80px' : '10px'};
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ const WebviewContainer = memo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const WebviewStyle: React.CSSProperties = {
|
const WebviewStyle: React.CSSProperties = {
|
||||||
width: 'calc(100vw - var(--sidebar-width))',
|
width: '100vw',
|
||||||
height: 'calc(100vh - var(--navbar-height))',
|
height: 'calc(100vh - var(--navbar-height))',
|
||||||
backgroundColor: 'var(--color-background)',
|
backgroundColor: 'var(--color-background)',
|
||||||
display: 'inline-flex'
|
display: 'inline-flex'
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { useAgents } from '@renderer/hooks/useAgents'
|
|
||||||
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistants, useDefaultAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useSystemAgents } from '@renderer/pages/agents'
|
import { useSystemAgents } from '@renderer/pages/agents'
|
||||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||||
@@ -24,7 +23,7 @@ interface Props {
|
|||||||
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { agents: userAgents } = useAgents()
|
const { assistants: userAgents } = useAssistants()
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
const { defaultAssistant } = useDefaultAssistant()
|
const { defaultAssistant } = useDefaultAssistant()
|
||||||
const { assistants, addAssistant } = useAssistants()
|
const { assistants, addAssistant } = useAssistants()
|
||||||
|
|||||||
@@ -14,14 +14,7 @@ interface Props {
|
|||||||
position: 'left' | 'right'
|
position: 'left' | 'right'
|
||||||
}
|
}
|
||||||
|
|
||||||
const FloatingSidebar: FC<Props> = ({
|
const FloatingSidebar: FC<Props> = ({ children, activeAssistant, setActiveAssistant, activeTopic, setActiveTopic }) => {
|
||||||
children,
|
|
||||||
activeAssistant,
|
|
||||||
setActiveAssistant,
|
|
||||||
activeTopic,
|
|
||||||
setActiveTopic,
|
|
||||||
position = 'left'
|
|
||||||
}) => {
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
useHotkeys('esc', () => {
|
useHotkeys('esc', () => {
|
||||||
@@ -45,12 +38,11 @@ const FloatingSidebar: FC<Props> = ({
|
|||||||
const content = (
|
const content = (
|
||||||
<PopoverContent maxHeight={maxHeight}>
|
<PopoverContent maxHeight={maxHeight}>
|
||||||
<HomeTabs
|
<HomeTabs
|
||||||
|
tab="assistants"
|
||||||
activeAssistant={activeAssistant}
|
activeAssistant={activeAssistant}
|
||||||
activeTopic={activeTopic}
|
activeTopic={activeTopic}
|
||||||
setActiveAssistant={setActiveAssistant}
|
setActiveAssistant={setActiveAssistant}
|
||||||
setActiveTopic={setActiveTopic}
|
setActiveTopic={setActiveTopic}
|
||||||
position={position}
|
|
||||||
forceToSeeAllTab={true}
|
|
||||||
style={{
|
style={{
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
|||||||
375
src/renderer/src/components/Popups/MessageSettingsPopup.tsx
Normal file
375
src/renderer/src/components/Popups/MessageSettingsPopup.tsx
Normal file
@@ -0,0 +1,375 @@
|
|||||||
|
import { useCodeStyle } from '@renderer/context/CodeStyleProvider'
|
||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { SettingDivider, SettingRow, SettingRowTitle } from '@renderer/pages/settings'
|
||||||
|
import { useAppDispatch } from '@renderer/store'
|
||||||
|
import {
|
||||||
|
setCodeCollapsible,
|
||||||
|
setCodeEditor,
|
||||||
|
setCodeExecution,
|
||||||
|
setCodePreview,
|
||||||
|
setCodeShowLineNumbers,
|
||||||
|
setCodeWrappable,
|
||||||
|
setFontSize,
|
||||||
|
setMathEngine,
|
||||||
|
setMessageFont,
|
||||||
|
setMessageNavigation,
|
||||||
|
setMessageStyle,
|
||||||
|
setMultiModelMessageStyle,
|
||||||
|
setShowMessageDivider,
|
||||||
|
setThoughtAutoCollapse
|
||||||
|
} from '@renderer/store/settings'
|
||||||
|
import { CodeStyleVarious, MathEngine, ThemeMode } from '@renderer/types'
|
||||||
|
import { Col, InputNumber, Modal, Row, Slider, Switch, Tooltip } from 'antd'
|
||||||
|
import { CircleHelp } from 'lucide-react'
|
||||||
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import Selector from '../Selector'
|
||||||
|
import { TopView } from '../TopView'
|
||||||
|
|
||||||
|
interface ShowParams {
|
||||||
|
title: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends ShowParams {
|
||||||
|
resolve: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
|
||||||
|
const { messageStyle, fontSize } = useSettings()
|
||||||
|
const { theme } = useTheme()
|
||||||
|
const { themeNames } = useCodeStyle()
|
||||||
|
|
||||||
|
const [fontSizeValue, setFontSizeValue] = useState(fontSize)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const {
|
||||||
|
showMessageDivider,
|
||||||
|
messageFont,
|
||||||
|
codeShowLineNumbers,
|
||||||
|
codeCollapsible,
|
||||||
|
codeWrappable,
|
||||||
|
codeEditor,
|
||||||
|
codePreview,
|
||||||
|
codeExecution,
|
||||||
|
mathEngine,
|
||||||
|
multiModelMessageStyle,
|
||||||
|
thoughtAutoCollapse,
|
||||||
|
messageNavigation
|
||||||
|
} = useSettings()
|
||||||
|
|
||||||
|
const codeStyle = useMemo(() => {
|
||||||
|
return codeEditor.enabled
|
||||||
|
? theme === ThemeMode.light
|
||||||
|
? codeEditor.themeLight
|
||||||
|
: codeEditor.themeDark
|
||||||
|
: theme === ThemeMode.light
|
||||||
|
? codePreview.themeLight
|
||||||
|
: codePreview.themeDark
|
||||||
|
}, [
|
||||||
|
codeEditor.enabled,
|
||||||
|
codeEditor.themeLight,
|
||||||
|
codeEditor.themeDark,
|
||||||
|
theme,
|
||||||
|
codePreview.themeLight,
|
||||||
|
codePreview.themeDark
|
||||||
|
])
|
||||||
|
|
||||||
|
const onCodeStyleChange = useCallback(
|
||||||
|
(value: CodeStyleVarious) => {
|
||||||
|
const field = theme === ThemeMode.light ? 'themeLight' : 'themeDark'
|
||||||
|
const action = codeEditor.enabled ? setCodeEditor : setCodePreview
|
||||||
|
dispatch(action({ [field]: value }))
|
||||||
|
},
|
||||||
|
[dispatch, theme, codeEditor.enabled]
|
||||||
|
)
|
||||||
|
|
||||||
|
const onOk = () => {
|
||||||
|
resolve(true)
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
resolve(false)
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageSettingsPopup.hide = onCancel
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={title}
|
||||||
|
open={open}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
transitionName="animation-move-down"
|
||||||
|
footer={null}
|
||||||
|
centered
|
||||||
|
styles={{ body: { maxHeight: '70vh', overflowY: 'auto' } }}>
|
||||||
|
<SettingGroup>
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('settings.messages.divider')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={showMessageDivider}
|
||||||
|
onChange={(checked) => dispatch(setShowMessageDivider(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('settings.messages.use_serif_font')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={messageFont === 'serif'}
|
||||||
|
onChange={(checked) => dispatch(setMessageFont(checked ? 'serif' : 'system'))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>
|
||||||
|
{t('chat.settings.thought_auto_collapse')}
|
||||||
|
<Tooltip title={t('chat.settings.thought_auto_collapse.tip')}>
|
||||||
|
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={thoughtAutoCollapse}
|
||||||
|
onChange={(checked) => dispatch(setThoughtAutoCollapse(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('message.message.style')}</SettingRowTitleSmall>
|
||||||
|
<Selector
|
||||||
|
value={messageStyle}
|
||||||
|
onChange={(value) => dispatch(setMessageStyle(value as 'plain' | 'bubble'))}
|
||||||
|
options={[
|
||||||
|
{ label: t('message.message.style.plain'), value: 'plain' },
|
||||||
|
{ label: t('message.message.style.bubble'), value: 'bubble' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('message.message.multi_model_style')}</SettingRowTitleSmall>
|
||||||
|
<Selector
|
||||||
|
value={multiModelMessageStyle}
|
||||||
|
onChange={(value) =>
|
||||||
|
dispatch(setMultiModelMessageStyle(value as 'fold' | 'vertical' | 'horizontal' | 'grid'))
|
||||||
|
}
|
||||||
|
options={[
|
||||||
|
{ label: t('message.message.multi_model_style.fold'), value: 'fold' },
|
||||||
|
{ label: t('message.message.multi_model_style.vertical'), value: 'vertical' },
|
||||||
|
{ label: t('message.message.multi_model_style.horizontal'), value: 'horizontal' },
|
||||||
|
{ label: t('message.message.multi_model_style.grid'), value: 'grid' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('settings.messages.navigation')}</SettingRowTitleSmall>
|
||||||
|
<Selector
|
||||||
|
value={messageNavigation}
|
||||||
|
onChange={(value) => dispatch(setMessageNavigation(value as 'none' | 'buttons' | 'anchor'))}
|
||||||
|
options={[
|
||||||
|
{ label: t('settings.messages.navigation.none'), value: 'none' },
|
||||||
|
{ label: t('settings.messages.navigation.buttons'), value: 'buttons' },
|
||||||
|
{ label: t('settings.messages.navigation.anchor'), value: 'anchor' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('settings.messages.math_engine')}</SettingRowTitleSmall>
|
||||||
|
<Selector
|
||||||
|
value={mathEngine}
|
||||||
|
onChange={(value) => dispatch(setMathEngine(value as MathEngine))}
|
||||||
|
options={[
|
||||||
|
{ label: 'KaTeX', value: 'KaTeX' },
|
||||||
|
{ label: 'MathJax', value: 'MathJax' },
|
||||||
|
{ label: t('settings.messages.math_engine.none'), value: 'none' }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('settings.font_size.title')}</SettingRowTitleSmall>
|
||||||
|
</SettingRow>
|
||||||
|
<Row align="middle" gutter={10}>
|
||||||
|
<Col span={24}>
|
||||||
|
<Slider
|
||||||
|
value={fontSizeValue}
|
||||||
|
onChange={(value) => setFontSizeValue(value)}
|
||||||
|
onChangeComplete={(value) => dispatch(setFontSize(value))}
|
||||||
|
min={12}
|
||||||
|
max={22}
|
||||||
|
step={1}
|
||||||
|
marks={{
|
||||||
|
12: <span style={{ fontSize: '12px' }}>A</span>,
|
||||||
|
14: <span style={{ fontSize: '14px' }}>{t('common.default')}</span>,
|
||||||
|
22: <span style={{ fontSize: '18px' }}>A</span>
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</SettingGroup>
|
||||||
|
<SettingGroup>
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
|
||||||
|
<Selector
|
||||||
|
value={codeStyle}
|
||||||
|
onChange={(value) => onCodeStyleChange(value as CodeStyleVarious)}
|
||||||
|
options={themeNames.map((theme) => ({ label: theme, value: theme }))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>
|
||||||
|
{t('chat.settings.code_execution.title')}
|
||||||
|
<Tooltip title={t('chat.settings.code_execution.tip')}>
|
||||||
|
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={codeExecution.enabled}
|
||||||
|
onChange={(checked) => dispatch(setCodeExecution({ enabled: checked }))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
{codeExecution.enabled && (
|
||||||
|
<>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow style={{ paddingLeft: 8 }}>
|
||||||
|
<SettingRowTitleSmall>
|
||||||
|
{t('chat.settings.code_execution.timeout_minutes')}
|
||||||
|
<Tooltip title={t('chat.settings.code_execution.timeout_minutes.tip')}>
|
||||||
|
<CircleHelp size={14} style={{ marginLeft: 4 }} color="var(--color-text-2)" />
|
||||||
|
</Tooltip>
|
||||||
|
</SettingRowTitleSmall>
|
||||||
|
<InputNumber
|
||||||
|
size="small"
|
||||||
|
min={1}
|
||||||
|
max={60}
|
||||||
|
step={1}
|
||||||
|
value={codeExecution.timeoutMinutes}
|
||||||
|
onChange={(value) => dispatch(setCodeExecution({ timeoutMinutes: value ?? 1 }))}
|
||||||
|
style={{ width: 80 }}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('chat.settings.code_editor.title')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={codeEditor.enabled}
|
||||||
|
onChange={(checked) => dispatch(setCodeEditor({ enabled: checked }))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
{codeEditor.enabled && (
|
||||||
|
<>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow style={{ paddingLeft: 8 }}>
|
||||||
|
<SettingRowTitleSmall>{t('chat.settings.code_editor.highlight_active_line')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={codeEditor.highlightActiveLine}
|
||||||
|
onChange={(checked) => dispatch(setCodeEditor({ highlightActiveLine: checked }))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow style={{ paddingLeft: 8 }}>
|
||||||
|
<SettingRowTitleSmall>{t('chat.settings.code_editor.fold_gutter')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={codeEditor.foldGutter}
|
||||||
|
onChange={(checked) => dispatch(setCodeEditor({ foldGutter: checked }))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow style={{ paddingLeft: 8 }}>
|
||||||
|
<SettingRowTitleSmall>{t('chat.settings.code_editor.autocompletion')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={codeEditor.autocompletion}
|
||||||
|
onChange={(checked) => dispatch(setCodeEditor({ autocompletion: checked }))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow style={{ paddingLeft: 8 }}>
|
||||||
|
<SettingRowTitleSmall>{t('chat.settings.code_editor.keymap')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={codeEditor.keymap}
|
||||||
|
onChange={(checked) => dispatch(setCodeEditor({ keymap: checked }))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('chat.settings.show_line_numbers')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={codeShowLineNumbers}
|
||||||
|
onChange={(checked) => dispatch(setCodeShowLineNumbers(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('chat.settings.code_collapsible')}</SettingRowTitleSmall>
|
||||||
|
<Switch
|
||||||
|
size="small"
|
||||||
|
checked={codeCollapsible}
|
||||||
|
onChange={(checked) => dispatch(setCodeCollapsible(checked))}
|
||||||
|
/>
|
||||||
|
</SettingRow>
|
||||||
|
<SettingDivider />
|
||||||
|
<SettingRow>
|
||||||
|
<SettingRowTitleSmall>{t('chat.settings.code_wrappable')}</SettingRowTitleSmall>
|
||||||
|
<Switch size="small" checked={codeWrappable} onChange={(checked) => dispatch(setCodeWrappable(checked))} />
|
||||||
|
</SettingRow>
|
||||||
|
</SettingGroup>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingRowTitleSmall = styled(SettingRowTitle)`
|
||||||
|
font-size: 13px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const SettingGroup = styled.div<{ theme?: ThemeMode }>`
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TopViewKey = 'MessageSettingsPopup'
|
||||||
|
|
||||||
|
export default class MessageSettingsPopup {
|
||||||
|
static topviewId = 0
|
||||||
|
static hide() {
|
||||||
|
TopView.hide(TopViewKey)
|
||||||
|
}
|
||||||
|
static show(props: ShowParams) {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
TopView.show(<PopupContainer {...props} resolve={resolve} />, TopViewKey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/renderer/src/components/Popups/SettingsPopup.tsx
Normal file
24
src/renderer/src/components/Popups/SettingsPopup.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export interface SettingsPopupShowParams {
|
||||||
|
defaultTab?:
|
||||||
|
| 'provider'
|
||||||
|
| 'model'
|
||||||
|
| 'tool'
|
||||||
|
| 'general'
|
||||||
|
| 'display'
|
||||||
|
| 'shortcut'
|
||||||
|
| 'quickAssistant'
|
||||||
|
| 'selectionAssistant'
|
||||||
|
| 'data'
|
||||||
|
| 'about'
|
||||||
|
| 'quickPhrase'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class SettingsPopup {
|
||||||
|
static hide() {
|
||||||
|
// Settings window is now independent, user can close it manually
|
||||||
|
}
|
||||||
|
|
||||||
|
static show(props: SettingsPopupShowParams = {}) {
|
||||||
|
return window.api.showSettingsWindow({ defaultTab: props.defaultTab })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,15 +15,17 @@ const PopupContainer: React.FC<Props> = ({ title, resolve }) => {
|
|||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
|
|
||||||
const onOk = () => {
|
const onOk = () => {
|
||||||
|
resolve(true)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
|
resolve(false)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
resolve({})
|
TopView.hide(TopViewKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
TemplatePopup.hide = onCancel
|
TemplatePopup.hide = onCancel
|
||||||
@@ -51,16 +53,7 @@ export default class TemplatePopup {
|
|||||||
}
|
}
|
||||||
static show(props: ShowParams) {
|
static show(props: ShowParams) {
|
||||||
return new Promise<any>((resolve) => {
|
return new Promise<any>((resolve) => {
|
||||||
TopView.show(
|
TopView.show(<PopupContainer {...props} resolve={resolve} />, TopViewKey)
|
||||||
<PopupContainer
|
|
||||||
{...props}
|
|
||||||
resolve={(v) => {
|
|
||||||
resolve(v)
|
|
||||||
TopView.hide(TopViewKey)
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
TopViewKey
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
237
src/renderer/src/components/Tabs/TabsContainer.tsx
Normal file
237
src/renderer/src/components/Tabs/TabsContainer.tsx
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
import { PlusOutlined } from '@ant-design/icons'
|
||||||
|
import { isMac } from '@renderer/config/constant'
|
||||||
|
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||||
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import type { Tab } from '@renderer/store/tabs'
|
||||||
|
import { addTab, removeTab, setActiveTab } from '@renderer/store/tabs'
|
||||||
|
import {
|
||||||
|
FileSearch,
|
||||||
|
Folder,
|
||||||
|
Home,
|
||||||
|
Languages,
|
||||||
|
LayoutGrid,
|
||||||
|
Palette,
|
||||||
|
Settings,
|
||||||
|
Sparkle,
|
||||||
|
SquareTerminal,
|
||||||
|
X
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface TabsContainerProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTabIcon = (tabId: string): React.ReactNode | undefined => {
|
||||||
|
switch (tabId) {
|
||||||
|
case 'home':
|
||||||
|
return <Home size={14} />
|
||||||
|
case 'agents':
|
||||||
|
return <Sparkle size={14} />
|
||||||
|
case 'translate':
|
||||||
|
return <Languages size={14} />
|
||||||
|
case 'paintings':
|
||||||
|
return <Palette size={14} />
|
||||||
|
case 'apps':
|
||||||
|
return <LayoutGrid size={14} />
|
||||||
|
case 'knowledge':
|
||||||
|
return <FileSearch size={14} />
|
||||||
|
case 'mcp':
|
||||||
|
return <SquareTerminal size={14} />
|
||||||
|
case 'files':
|
||||||
|
return <Folder size={14} />
|
||||||
|
case 'settings':
|
||||||
|
return <Settings size={14} />
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const TabsContainer: React.FC<TabsContainerProps> = ({ children }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const location = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const tabs = useAppSelector((state) => state.tabs.tabs)
|
||||||
|
const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
|
||||||
|
const isFullscreen = useFullscreen()
|
||||||
|
|
||||||
|
const getTabId = (path: string): string => {
|
||||||
|
if (path === '/') return 'home'
|
||||||
|
const segments = path.split('/')
|
||||||
|
return segments[1] // 获取第一个路径段作为 id
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldCreateTab = (path: string) => {
|
||||||
|
if (path === '/') return false
|
||||||
|
return !tabs.some((tab) => tab.id === getTabId(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const tabId = getTabId(location.pathname)
|
||||||
|
const currentTab = tabs.find((tab) => tab.id === tabId)
|
||||||
|
|
||||||
|
if (!currentTab && shouldCreateTab(location.pathname)) {
|
||||||
|
dispatch(
|
||||||
|
addTab({
|
||||||
|
id: tabId,
|
||||||
|
path: location.pathname
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} else if (currentTab) {
|
||||||
|
dispatch(setActiveTab(currentTab.id))
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [dispatch, location.pathname])
|
||||||
|
|
||||||
|
const closeTab = (tabId: string) => {
|
||||||
|
const tabToClose = tabs.find((tab) => tab.id === tabId)
|
||||||
|
if (!tabToClose) return
|
||||||
|
|
||||||
|
if (tabs.length === 1) return
|
||||||
|
|
||||||
|
if (tabId === activeTabId) {
|
||||||
|
const remainingTabs = tabs.filter((tab) => tab.id !== tabId)
|
||||||
|
const lastTab = remainingTabs[remainingTabs.length - 1]
|
||||||
|
navigate(lastTab.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(removeTab(tabId))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddTab = () => {
|
||||||
|
navigate('/launchpad')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<TabsBar $isFullscreen={isFullscreen}>
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Tab key={tab.id} active={tab.id === activeTabId} onClick={() => navigate(tab.path)}>
|
||||||
|
<TabHeader>
|
||||||
|
{tab.id && <TabIcon>{getTabIcon(tab.id)}</TabIcon>}
|
||||||
|
<TabTitle>{t(`title.${tab.id}`)}</TabTitle>
|
||||||
|
</TabHeader>
|
||||||
|
{tab.id !== 'home' && (
|
||||||
|
<CloseButton
|
||||||
|
className="close-button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
closeTab(tab.id)
|
||||||
|
}}>
|
||||||
|
<X size={12} />
|
||||||
|
</CloseButton>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
<AddTabButton onClick={handleAddTab}>
|
||||||
|
<PlusOutlined />
|
||||||
|
</AddTabButton>
|
||||||
|
</TabsBar>
|
||||||
|
<TabContent>{children}</TabContent>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TabsBar = styled.div<{ $isFullscreen: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding-left: ${({ $isFullscreen }) => (!$isFullscreen && isMac ? '80px' : '8px')};
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
height: var(--navbar-height);
|
||||||
|
`
|
||||||
|
|
||||||
|
const Tab = styled.div<{ active?: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 4px 10px;
|
||||||
|
background: ${(props) => (props.active ? 'var(--color-background)' : 'transparent')};
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-app-region: none;
|
||||||
|
min-width: 100px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
.close-button {
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
margin-right: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${(props) => (props.active ? 'var(--color-background)' : 'var(--color-background-soft)')};
|
||||||
|
.close-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TabHeader = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TabIcon = styled.span`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 6px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
`
|
||||||
|
|
||||||
|
const TabTitle = styled.span`
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-right: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const CloseButton = styled.span`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const AddTabButton = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-text);
|
||||||
|
-webkit-app-region: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-background-soft);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TabContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
width: calc(100vw - 12px);
|
||||||
|
margin: 6px;
|
||||||
|
margin-top: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default TabsContainer
|
||||||
@@ -80,7 +80,7 @@ const TranslateButton: FC<Props> = ({ text, onTranslated, disabled, style, isLoa
|
|||||||
mouseLeaveDelay={0}
|
mouseLeaveDelay={0}
|
||||||
arrow>
|
arrow>
|
||||||
<ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text">
|
<ToolbarButton onClick={handleTranslate} disabled={disabled || isTranslating} style={style} type="text">
|
||||||
{isTranslating ? <LoadingOutlined spin /> : <Languages size={18} />}
|
{isTranslating ? <LoadingOutlined spin /> : <Languages size={16} />}
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
import { isLinux, isMac, isWin } from '@renderer/config/constant'
|
import { isLinux, isMac, isWin } from '@renderer/config/constant'
|
||||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||||
import useNavBackgroundColor from '@renderer/hooks/useNavBackgroundColor'
|
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||||
import type { FC, PropsWithChildren } from 'react'
|
import type { FC, HTMLAttributes, PropsWithChildren } from 'react'
|
||||||
import type { HTMLAttributes } from 'react'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
|
type Props = PropsWithChildren & HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
export const Navbar: FC<Props> = ({ children, ...props }) => {
|
||||||
const backgroundColor = useNavBackgroundColor()
|
return <NavbarContainer {...props}>{children}</NavbarContainer>
|
||||||
|
|
||||||
return (
|
|
||||||
<NavbarContainer {...props} style={{ backgroundColor }}>
|
|
||||||
{children}
|
|
||||||
</NavbarContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NavbarLeft: FC<Props> = ({ children, ...props }) => {
|
|
||||||
return <NavbarLeftContainer {...props}>{children}</NavbarLeftContainer>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const NavbarCenter: FC<Props> = ({ children, ...props }) => {
|
export const NavbarCenter: FC<Props> = ({ children, ...props }) => {
|
||||||
@@ -36,8 +25,10 @@ export const NavbarRight: FC<Props> = ({ children, ...props }) => {
|
|||||||
|
|
||||||
export const NavbarMain: FC<Props> = ({ children, ...props }) => {
|
export const NavbarMain: FC<Props> = ({ children, ...props }) => {
|
||||||
const isFullscreen = useFullscreen()
|
const isFullscreen = useFullscreen()
|
||||||
|
const { showAssistants } = useShowAssistants()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavbarMainContainer {...props} $isFullscreen={isFullscreen}>
|
<NavbarMainContainer {...props} $isFullscreen={isFullscreen} $showAssistants={showAssistants}>
|
||||||
{children}
|
{children}
|
||||||
</NavbarMainContainer>
|
</NavbarMainContainer>
|
||||||
)
|
)
|
||||||
@@ -49,28 +40,8 @@ const NavbarContainer = styled.div`
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
min-height: var(--navbar-height);
|
min-height: var(--navbar-height);
|
||||||
max-height: var(--navbar-height);
|
max-height: var(--navbar-height);
|
||||||
margin-left: ${isMac ? 'calc(var(--sidebar-width) * -1)' : 0};
|
|
||||||
padding-left: ${isMac ? 'var(--sidebar-width)' : 0};
|
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
`
|
background-color: var(--color-background);
|
||||||
|
|
||||||
const NavbarLeftContainer = styled.div`
|
|
||||||
min-width: var(--assistants-width);
|
|
||||||
padding: 0 10px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
`
|
|
||||||
|
|
||||||
const NavbarCenterContainer = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0 ${isMac ? '20px' : 0};
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--color-text-1);
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
|
const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
|
||||||
@@ -82,14 +53,45 @@ const NavbarRightContainer = styled.div<{ $isFullscreen: boolean }>`
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
`
|
`
|
||||||
|
|
||||||
const NavbarMainContainer = styled.div<{ $isFullscreen: boolean }>`
|
const NavbarMainContainer = styled.div<{ $isFullscreen: boolean; $showAssistants: boolean }>`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
height: var(--navbar-height);
|
||||||
|
max-height: var(--navbar-height);
|
||||||
|
min-height: var(--navbar-height);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0 ${isMac ? '20px' : 0};
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--color-text-1);
|
color: var(--color-text-1);
|
||||||
padding-right: ${({ $isFullscreen }) => ($isFullscreen ? '12px' : isWin ? '140px' : isLinux ? '120px' : '12px')};
|
-webkit-app-region: drag;
|
||||||
|
padding: 0 12px;
|
||||||
|
padding-left: ${({ $showAssistants }) => (isMac && !$showAssistants ? '70px' : '10px')};
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const NavbarCenterContainer = styled.div`
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: var(--navbar-height);
|
||||||
|
max-height: var(--navbar-height);
|
||||||
|
min-height: var(--navbar-height);
|
||||||
|
padding: 0 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
justify-content: space-between;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
`
|
||||||
|
|
||||||
|
// const rotateAnimation = keyframes`
|
||||||
|
// from {
|
||||||
|
// transform: rotate(-180deg);
|
||||||
|
// }
|
||||||
|
// to {
|
||||||
|
// transform: rotate(0);
|
||||||
|
// }
|
||||||
|
// `
|
||||||
|
|
||||||
|
// const AnimatedButton = styled(Button)`
|
||||||
|
// animation: ${rotateAnimation} 0.4s ease-out;
|
||||||
|
// `
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
|
|
||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
import { AppLogo } from '@renderer/config/env'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import useAvatar from '@renderer/hooks/useAvatar'
|
|
||||||
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
import { useFullscreen } from '@renderer/hooks/useFullscreen'
|
||||||
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
@@ -11,9 +9,8 @@ import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
|||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import { ThemeMode } from '@renderer/types'
|
import { ThemeMode } from '@renderer/types'
|
||||||
import { isEmoji } from '@renderer/utils'
|
|
||||||
import type { MenuProps } from 'antd'
|
import type { MenuProps } from 'antd'
|
||||||
import { Avatar, Dropdown, Tooltip } from 'antd'
|
import { Dropdown, Tooltip } from 'antd'
|
||||||
import {
|
import {
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
FileSearch,
|
FileSearch,
|
||||||
@@ -35,7 +32,6 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
import { DraggableList } from '../DraggableList'
|
import { DraggableList } from '../DraggableList'
|
||||||
import MinAppIcon from '../Icons/MinAppIcon'
|
import MinAppIcon from '../Icons/MinAppIcon'
|
||||||
import UserPopup from '../Popups/UserPopup'
|
|
||||||
|
|
||||||
const Sidebar: FC = () => {
|
const Sidebar: FC = () => {
|
||||||
const { hideMinappPopup, openMinapp } = useMinappPopup()
|
const { hideMinappPopup, openMinapp } = useMinappPopup()
|
||||||
@@ -47,11 +43,8 @@ const Sidebar: FC = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { theme, settedTheme, toggleTheme } = useTheme()
|
const { theme, settedTheme, toggleTheme } = useTheme()
|
||||||
const avatar = useAvatar()
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const onEditUser = () => UserPopup.show()
|
|
||||||
|
|
||||||
const backgroundColor = useNavBackgroundColor()
|
const backgroundColor = useNavBackgroundColor()
|
||||||
|
|
||||||
const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
|
const showPinnedApps = pinned.length > 0 && sidebarIcons.visible.includes('minapp')
|
||||||
@@ -79,13 +72,6 @@ const Sidebar: FC = () => {
|
|||||||
$isFullscreen={isFullscreen}
|
$isFullscreen={isFullscreen}
|
||||||
id="app-sidebar"
|
id="app-sidebar"
|
||||||
style={{ backgroundColor, zIndex: minappShow ? 10000 : 'initial' }}>
|
style={{ backgroundColor, zIndex: minappShow ? 10000 : 'initial' }}>
|
||||||
{isEmoji(avatar) ? (
|
|
||||||
<EmojiAvatar onClick={onEditUser} className="sidebar-avatar" size={31} fontSize={18}>
|
|
||||||
{avatar}
|
|
||||||
</EmojiAvatar>
|
|
||||||
) : (
|
|
||||||
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" onClick={onEditUser} />
|
|
||||||
)}
|
|
||||||
<MainMenusContainer>
|
<MainMenusContainer>
|
||||||
<Menus onClick={hideMinappPopup}>
|
<Menus onClick={hideMinappPopup}>
|
||||||
<MainMenus />
|
<MainMenus />
|
||||||
@@ -339,16 +325,6 @@ const Container = styled.div<{ $isFullscreen: boolean }>`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const AvatarImg = styled(Avatar)`
|
|
||||||
width: 31px;
|
|
||||||
height: 31px;
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
margin-bottom: ${isMac ? '12px' : '12px'};
|
|
||||||
margin-top: ${isMac ? '0px' : '2px'};
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
`
|
|
||||||
|
|
||||||
const MainMenusContainer = styled.div`
|
const MainMenusContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ As [role name], with [list skills], strictly adhering to [list constraints], usi
|
|||||||
`
|
`
|
||||||
|
|
||||||
export const SUMMARIZE_PROMPT =
|
export const SUMMARIZE_PROMPT =
|
||||||
"You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks or other special symbols"
|
"You are an assistant skilled in conversation. You need to summarize the user's conversation into a title within 10 words. The language of the title should be consistent with the user's primary language. Do not use punctuation marks, markdown language markers, or other special symbols"
|
||||||
|
|
||||||
// https://github.com/ItzCrazyKns/Perplexica/blob/master/src/lib/prompts/webSearch.ts
|
// https://github.com/ItzCrazyKns/Perplexica/blob/master/src/lib/prompts/webSearch.ts
|
||||||
export const SEARCH_SUMMARY_PROMPT = `
|
export const SEARCH_SUMMARY_PROMPT = `
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ interface ThemeProviderProps extends PropsWithChildren {
|
|||||||
|
|
||||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||||
// 用户设置的主题
|
// 用户设置的主题
|
||||||
const { theme: settedTheme, setTheme: setSettedTheme } = useSettings()
|
const { theme: settedTheme, setTheme: setSettedTheme, transparentWindow } = useSettings()
|
||||||
const [actualTheme, setActualTheme] = useState<ThemeMode>(
|
const [actualTheme, setActualTheme] = useState<ThemeMode>(
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches ? ThemeMode.dark : ThemeMode.light
|
window.matchMedia('(prefers-color-scheme: dark)').matches ? ThemeMode.dark : ThemeMode.light
|
||||||
)
|
)
|
||||||
@@ -56,7 +56,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
|||||||
document.body.setAttribute('theme-mode', actualTheme)
|
document.body.setAttribute('theme-mode', actualTheme)
|
||||||
setActualTheme(actualTheme)
|
setActualTheme(actualTheme)
|
||||||
})
|
})
|
||||||
}, [actualTheme, initUserTheme, setSettedTheme, settedTheme])
|
}, [actualTheme, initUserTheme, setSettedTheme, settedTheme, transparentWindow])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.api.setTheme(settedTheme)
|
window.api.setTheme(settedTheme)
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
|
import SettingsPopup from '@renderer/components/Popups/SettingsPopup'
|
||||||
|
import NavigationService from '@renderer/services/NavigationService'
|
||||||
import { useAppSelector } from '@renderer/store'
|
import { useAppSelector } from '@renderer/store'
|
||||||
|
import { useEffect } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
const NavigationHandler: React.FC = () => {
|
const NavigationHandler: React.FC = () => {
|
||||||
const location = useLocation()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const showSettingsShortcutEnabled = useAppSelector(
|
const showSettingsShortcutEnabled = useAppSelector(
|
||||||
(state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled
|
(state) => state.shortcuts.shortcuts.find((s) => s.key === 'show_settings')?.enabled
|
||||||
)
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
NavigationService.setNavigate(navigate)
|
||||||
|
}, [navigate])
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'meta+, ! ctrl+,',
|
'meta+, ! ctrl+,',
|
||||||
function () {
|
function () {
|
||||||
if (location.pathname.startsWith('/settings')) {
|
SettingsPopup.show({ defaultTab: 'provider' })
|
||||||
return
|
|
||||||
}
|
|
||||||
navigate('/settings/provider')
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
splitKey: '!',
|
splitKey: '!',
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
|
||||||
import { addAgent, removeAgent, updateAgent, updateAgents, updateAgentSettings } from '@renderer/store/agents'
|
|
||||||
import { Agent, AssistantSettings } from '@renderer/types'
|
|
||||||
|
|
||||||
export function useAgents() {
|
|
||||||
const agents = useAppSelector((state) => state.agents.agents)
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
return {
|
|
||||||
agents,
|
|
||||||
updateAgents: (agents: Agent[]) => dispatch(updateAgents(agents)),
|
|
||||||
addAgent: (agent: Agent) => dispatch(addAgent(agent)),
|
|
||||||
removeAgent: (id: string) => dispatch(removeAgent({ id }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useAgent(id: string) {
|
|
||||||
const agent = useAppSelector((state) => state.agents.agents.find((a) => a.id === id) as Agent)
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
|
|
||||||
return {
|
|
||||||
agent,
|
|
||||||
updateAgent: (agent: Agent) => dispatch(updateAgent(agent)),
|
|
||||||
updateAgentSettings: (settings: Partial<AssistantSettings>) => {
|
|
||||||
dispatch(updateAgentSettings({ assistantId: agent.id, settings }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import { isMac } from '@renderer/config/constant'
|
|
||||||
import { isLocalAi } from '@renderer/config/env'
|
import { isLocalAi } from '@renderer/config/env'
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import KnowledgeQueue from '@renderer/queue/KnowledgeQueue'
|
import KnowledgeQueue from '@renderer/queue/KnowledgeQueue'
|
||||||
@@ -13,17 +11,14 @@ import { useEffect } from 'react'
|
|||||||
|
|
||||||
import { useDefaultModel } from './useAssistant'
|
import { useDefaultModel } from './useAssistant'
|
||||||
import useFullScreenNotice from './useFullScreenNotice'
|
import useFullScreenNotice from './useFullScreenNotice'
|
||||||
import { useRuntime } from './useRuntime'
|
|
||||||
import { useSettings } from './useSettings'
|
import { useSettings } from './useSettings'
|
||||||
import useUpdateHandler from './useUpdateHandler'
|
import useUpdateHandler from './useUpdateHandler'
|
||||||
|
|
||||||
export function useAppInit() {
|
export function useAppInit() {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { proxyUrl, language, windowStyle, autoCheckUpdate, proxyMode, customCss, enableDataCollection } = useSettings()
|
const { proxyUrl, language, autoCheckUpdate, proxyMode, customCss, enableDataCollection } = useSettings()
|
||||||
const { minappShow } = useRuntime()
|
|
||||||
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
const { setDefaultModel, setTopicNamingModel, setTranslateModel } = useDefaultModel()
|
||||||
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
|
||||||
const { theme } = useTheme()
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.getElementById('spinner')?.remove()
|
document.getElementById('spinner')?.remove()
|
||||||
@@ -70,18 +65,6 @@ export function useAppInit() {
|
|||||||
i18n.changeLanguage(language || navigator.language || defaultLanguage)
|
i18n.changeLanguage(language || navigator.language || defaultLanguage)
|
||||||
}, [language])
|
}, [language])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const transparentWindow = windowStyle === 'transparent' && isMac && !minappShow
|
|
||||||
|
|
||||||
if (minappShow) {
|
|
||||||
window.root.style.background =
|
|
||||||
windowStyle === 'transparent' && isMac ? 'var(--color-background)' : 'var(--navbar-background)'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
window.root.style.background = transparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
|
|
||||||
}, [windowStyle, minappShow, theme])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isLocalAi) {
|
if (isLocalAi) {
|
||||||
const model = JSON.parse(import.meta.env.VITE_RENDERER_INTEGRATED_MODEL)
|
const model = JSON.parse(import.meta.env.VITE_RENDERER_INTEGRATED_MODEL)
|
||||||
|
|||||||
@@ -1,45 +1,56 @@
|
|||||||
import { db } from '@renderer/databases'
|
import { db } from '@renderer/databases'
|
||||||
import { getDefaultTopic } from '@renderer/services/AssistantService'
|
|
||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
addAssistant,
|
addAssistant,
|
||||||
addTopic,
|
createAssistantFromTemplate,
|
||||||
removeAllTopics,
|
|
||||||
removeAssistant,
|
removeAssistant,
|
||||||
removeTopic,
|
selectActiveAssistants,
|
||||||
|
selectTemplates,
|
||||||
setModel,
|
setModel,
|
||||||
updateAssistant,
|
updateAssistant,
|
||||||
updateAssistants,
|
updateAssistants,
|
||||||
updateAssistantSettings,
|
updateAssistantSettings,
|
||||||
updateDefaultAssistant,
|
updateDefaultAssistant
|
||||||
updateTopic,
|
|
||||||
updateTopics
|
|
||||||
} from '@renderer/store/assistants'
|
} from '@renderer/store/assistants'
|
||||||
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
|
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
|
||||||
|
import { selectTopicsForAssistant, topicsActions } from '@renderer/store/topics'
|
||||||
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
|
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
|
||||||
import { useCallback, useMemo } from 'react'
|
import { useCallback, useMemo } from 'react'
|
||||||
|
|
||||||
import { TopicManager } from './useTopic'
|
|
||||||
|
|
||||||
export function useAssistants() {
|
export function useAssistants() {
|
||||||
const { assistants } = useAppSelector((state) => state.assistants)
|
const assistants = useAppSelector(selectActiveAssistants)
|
||||||
|
const templates = useAppSelector(selectTemplates)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const getAssistantById = useCallback((id: string) => assistants.find((a) => a.id === id), [assistants])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assistants,
|
assistants,
|
||||||
|
templates,
|
||||||
|
getAssistantById,
|
||||||
updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)),
|
updateAssistants: (assistants: Assistant[]) => dispatch(updateAssistants(assistants)),
|
||||||
addAssistant: (assistant: Assistant) => dispatch(addAssistant(assistant)),
|
addAssistant: (assistant: Assistant) => {
|
||||||
|
dispatch(addAssistant({ ...assistant, isTemplate: false }))
|
||||||
|
dispatch(topicsActions.addDefaultTopic({ assistantId: assistant.id }))
|
||||||
|
},
|
||||||
|
addTemplate: (template: Assistant) => {
|
||||||
|
dispatch(addAssistant({ ...template, isTemplate: true }))
|
||||||
|
},
|
||||||
removeAssistant: (id: string) => {
|
removeAssistant: (id: string) => {
|
||||||
dispatch(removeAssistant({ id }))
|
dispatch(removeAssistant({ id }))
|
||||||
const assistant = assistants.find((a) => a.id === id)
|
// Remove all topics for this assistant
|
||||||
const topics = assistant?.topics || []
|
dispatch(topicsActions.removeAllTopics({ assistantId: id }))
|
||||||
topics.forEach(({ id }) => TopicManager.removeTopic(id))
|
},
|
||||||
|
createAssistantFromTemplate: (templateId: string, assistantId: string) => {
|
||||||
|
dispatch(createAssistantFromTemplate({ templateId, assistantId }))
|
||||||
|
dispatch(topicsActions.addDefaultTopic({ assistantId }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAssistant(id: string) {
|
export function useAssistant(id: string) {
|
||||||
const assistant = useAppSelector((state) => state.assistants.assistants.find((a) => a.id === id) as Assistant)
|
const assistant = useAppSelector((state) => state.assistants.assistants.find((a) => a.id === id) as Assistant)
|
||||||
|
const topics = useTopicsForAssistant(id)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const { defaultModel } = useDefaultModel()
|
const { defaultModel } = useDefaultModel()
|
||||||
|
|
||||||
@@ -48,19 +59,18 @@ export function useAssistant(id: string) {
|
|||||||
throw new Error(`Assistant model is not set for assistant with name: ${assistant?.name ?? 'unknown'}`)
|
throw new Error(`Assistant model is not set for assistant with name: ${assistant?.name ?? 'unknown'}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const assistantWithModel = useMemo(() => ({ ...assistant, model }), [assistant, model])
|
const assistantWithModel = useMemo(() => ({ ...assistant, model, topics }), [assistant, model, topics])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
assistant: assistantWithModel,
|
assistant: assistantWithModel,
|
||||||
model,
|
model,
|
||||||
addTopic: (topic: Topic) => dispatch(addTopic({ assistantId: assistant.id, topic })),
|
topics,
|
||||||
|
addTopic: (topic: Topic) => dispatch(topicsActions.addTopic({ assistantId: id, topic })),
|
||||||
removeTopic: (topic: Topic) => {
|
removeTopic: (topic: Topic) => {
|
||||||
TopicManager.removeTopic(topic.id)
|
dispatch(topicsActions.removeTopic({ assistantId: id, topicId: topic.id }))
|
||||||
dispatch(removeTopic({ assistantId: assistant.id, topic }))
|
|
||||||
},
|
},
|
||||||
moveTopic: (topic: Topic, toAssistant: Assistant) => {
|
moveTopic: (topic: Topic, toAssistant: Assistant) => {
|
||||||
dispatch(addTopic({ assistantId: toAssistant.id, topic: { ...topic, assistantId: toAssistant.id } }))
|
dispatch(topicsActions.moveTopic({ fromAssistantId: id, toAssistantId: toAssistant.id, topicId: topic.id }))
|
||||||
dispatch(removeTopic({ assistantId: assistant.id, topic }))
|
|
||||||
// update topic messages in database
|
// update topic messages in database
|
||||||
db.topics
|
db.topics
|
||||||
.where('id')
|
.where('id')
|
||||||
@@ -74,9 +84,9 @@ export function useAssistant(id: string) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateTopic: (topic: Topic) => dispatch(updateTopic({ assistantId: assistant.id, topic })),
|
updateTopic: (topic: Topic) => dispatch(topicsActions.updateTopic({ assistantId: id, topic })),
|
||||||
updateTopics: (topics: Topic[]) => dispatch(updateTopics({ assistantId: assistant.id, topics })),
|
updateTopics: (topics: Topic[]) => dispatch(topicsActions.updateTopics({ assistantId: id, topics })),
|
||||||
removeAllTopics: () => dispatch(removeAllTopics({ assistantId: assistant.id })),
|
removeAllTopics: () => dispatch(topicsActions.removeAllTopics({ assistantId: id })),
|
||||||
setModel: useCallback(
|
setModel: useCallback(
|
||||||
(model: Model) => assistant && dispatch(setModel({ assistantId: assistant?.id, model })),
|
(model: Model) => assistant && dispatch(setModel({ assistantId: assistant?.id, model })),
|
||||||
[assistant, dispatch]
|
[assistant, dispatch]
|
||||||
@@ -88,16 +98,19 @@ export function useAssistant(id: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useTopicsForAssistant(assistantId: string) {
|
||||||
|
return useAppSelector((state) => selectTopicsForAssistant(state, assistantId))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认助手模板
|
||||||
|
*/
|
||||||
export function useDefaultAssistant() {
|
export function useDefaultAssistant() {
|
||||||
const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant)
|
const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const memoizedTopics = useMemo(() => [getDefaultTopic(defaultAssistant.id)], [defaultAssistant.id])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
defaultAssistant: {
|
defaultAssistant,
|
||||||
...defaultAssistant,
|
|
||||||
topics: memoizedTopics
|
|
||||||
},
|
|
||||||
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
|
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
92
src/renderer/src/hooks/useChat.tsx
Normal file
92
src/renderer/src/hooks/useChat.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||||
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
|
import { createContext, use, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { useTopicsForAssistant } from './useAssistant'
|
||||||
|
import { useSettings } from './useSettings'
|
||||||
|
|
||||||
|
interface ChatContextType {
|
||||||
|
activeAssistant: Assistant
|
||||||
|
activeTopic: Topic
|
||||||
|
setActiveAssistant: (assistant: Assistant) => void
|
||||||
|
setActiveTopic: (topic: Topic) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChatContext = createContext<ChatContextType | null>(null)
|
||||||
|
|
||||||
|
export const ChatProvider = ({ children }) => {
|
||||||
|
const assistants = useAppSelector((state) => state.assistants.assistants)
|
||||||
|
const [activeAssistant, setActiveAssistantBase] = useState<Assistant>(assistants[0])
|
||||||
|
const topics = useTopicsForAssistant(activeAssistant.id)
|
||||||
|
const [activeTopic, setActiveTopic] = useState<Topic>(topics[0])
|
||||||
|
const { clickAssistantToShowTopic } = useSettings()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const location = useLocation()
|
||||||
|
|
||||||
|
// 包装setActiveAssistant以添加导航逻辑
|
||||||
|
const setActiveAssistant = useCallback(
|
||||||
|
(assistant: Assistant) => {
|
||||||
|
setActiveAssistantBase(assistant)
|
||||||
|
// 如果当前不在聊天页面,导航到聊天页面
|
||||||
|
if (location.pathname !== '/') {
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setActiveAssistantBase, location.pathname, navigate]
|
||||||
|
)
|
||||||
|
|
||||||
|
// 当 topics 变化时,如果当前 activeTopic 不在 topics 中,设置第一个 topic
|
||||||
|
useEffect(() => {
|
||||||
|
if (!topics.find((topic) => topic.id === activeTopic?.id)) {
|
||||||
|
const firstTopic = topics[0]
|
||||||
|
firstTopic && setActiveTopic(firstTopic)
|
||||||
|
}
|
||||||
|
}, [topics, activeTopic?.id])
|
||||||
|
|
||||||
|
// 当 activeTopic 变化时加载消息
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTopic) {
|
||||||
|
dispatch(loadTopicMessagesThunk(activeTopic.id))
|
||||||
|
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
|
||||||
|
}
|
||||||
|
}, [activeTopic, dispatch])
|
||||||
|
|
||||||
|
// 处理点击助手显示话题侧边栏
|
||||||
|
useEffect(() => {
|
||||||
|
if (clickAssistantToShowTopic) {
|
||||||
|
EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR)
|
||||||
|
}
|
||||||
|
}, [clickAssistantToShowTopic, activeAssistant])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscriptions = [
|
||||||
|
EventEmitter.on(EVENT_NAMES.SET_ASSISTANT, setActiveAssistant),
|
||||||
|
EventEmitter.on(EVENT_NAMES.SET_TOPIC, setActiveTopic)
|
||||||
|
]
|
||||||
|
return () => subscriptions.forEach((subscription) => subscription())
|
||||||
|
}, [setActiveAssistant])
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({
|
||||||
|
activeAssistant,
|
||||||
|
activeTopic,
|
||||||
|
setActiveAssistant,
|
||||||
|
setActiveTopic
|
||||||
|
}),
|
||||||
|
[activeAssistant, activeTopic, setActiveAssistant]
|
||||||
|
)
|
||||||
|
|
||||||
|
return <ChatContext value={value}>{children}</ChatContext>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useChat = () => {
|
||||||
|
const context = use(ChatContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useChat must be used within ChatProvider')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|||||||
import { RootState } from '@renderer/store'
|
import { RootState } from '@renderer/store'
|
||||||
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
|
||||||
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
import { selectMessagesForTopic } from '@renderer/store/newMessage'
|
||||||
import { setActiveTopic, setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime'
|
import { setSelectedMessageIds, toggleMultiSelectMode } from '@renderer/store/runtime'
|
||||||
import { Topic } from '@renderer/types'
|
import { Topic } from '@renderer/types'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -27,10 +27,6 @@ export const useChatContext = (activeTopic: Topic) => {
|
|||||||
return () => unsubscribe()
|
return () => unsubscribe()
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(setActiveTopic(activeTopic))
|
|
||||||
}, [dispatch, activeTopic])
|
|
||||||
|
|
||||||
const handleToggleMultiSelectMode = useCallback(
|
const handleToggleMultiSelectMode = useCallback(
|
||||||
(value: boolean) => {
|
(value: boolean) => {
|
||||||
dispatch(toggleMultiSelectMode(value))
|
dispatch(toggleMultiSelectMode(value))
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import { useCallback, useEffect, useState } from 'react'
|
|||||||
import { useDispatch, useSelector } from 'react-redux'
|
import { useDispatch, useSelector } from 'react-redux'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
|
|
||||||
import { useAgents } from './useAgents'
|
|
||||||
import { useAssistants } from './useAssistant'
|
import { useAssistants } from './useAssistant'
|
||||||
|
|
||||||
export const useKnowledge = (baseId: string) => {
|
export const useKnowledge = (baseId: string) => {
|
||||||
@@ -295,7 +294,6 @@ export const useKnowledgeBases = () => {
|
|||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const bases = useSelector((state: RootState) => state.knowledge.bases)
|
const bases = useSelector((state: RootState) => state.knowledge.bases)
|
||||||
const { assistants, updateAssistants } = useAssistants()
|
const { assistants, updateAssistants } = useAssistants()
|
||||||
const { agents, updateAgents } = useAgents()
|
|
||||||
|
|
||||||
const addKnowledgeBase = (base: KnowledgeBase) => {
|
const addKnowledgeBase = (base: KnowledgeBase) => {
|
||||||
dispatch(addBase(base))
|
dispatch(addBase(base))
|
||||||
@@ -319,19 +317,7 @@ export const useKnowledgeBases = () => {
|
|||||||
return assistant
|
return assistant
|
||||||
})
|
})
|
||||||
|
|
||||||
// remove agent knowledge_base
|
|
||||||
const _agents = agents.map((agent) => {
|
|
||||||
if (agent.knowledge_bases?.find((kb) => kb.id === baseId)) {
|
|
||||||
return {
|
|
||||||
...agent,
|
|
||||||
knowledge_bases: agent.knowledge_bases.filter((kb) => kb.id !== baseId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return agent
|
|
||||||
})
|
|
||||||
|
|
||||||
updateAssistants(_assistants)
|
updateAssistants(_assistants)
|
||||||
updateAgents(_agents)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateKnowledgeBases = (bases: KnowledgeBase[]) => {
|
const updateKnowledgeBases = (bases: KnowledgeBase[]) => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
|
||||||
import { useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值
|
import { useSettings } from '@renderer/hooks/useSettings' // 使用设置中的值
|
||||||
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import {
|
import {
|
||||||
setCurrentMinappId,
|
setCurrentMinappId,
|
||||||
@@ -33,6 +34,7 @@ export const useMinappPopup = () => {
|
|||||||
/** Open a minapp (popup shows and minapp loaded) */
|
/** Open a minapp (popup shows and minapp loaded) */
|
||||||
const openMinapp = useCallback(
|
const openMinapp = useCallback(
|
||||||
(app: MinAppType, keepAlive: boolean = false) => {
|
(app: MinAppType, keepAlive: boolean = false) => {
|
||||||
|
EventEmitter.emit(EVENT_NAMES.OPEN_MINAPP, app)
|
||||||
if (keepAlive) {
|
if (keepAlive) {
|
||||||
// 如果小程序已经打开,只切换显示
|
// 如果小程序已经打开,只切换显示
|
||||||
if (openedKeepAliveMinapps.some((item) => item.id === app.id)) {
|
if (openedKeepAliveMinapps.some((item) => item.id === app.id)) {
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { isMac } from '@renderer/config/constant'
|
import { isMac } from '@renderer/config/constant'
|
||||||
|
|
||||||
import { useSettings } from './useSettings'
|
|
||||||
|
|
||||||
function useNavBackgroundColor() {
|
function useNavBackgroundColor() {
|
||||||
const { windowStyle } = useSettings()
|
if (isMac) {
|
||||||
|
|
||||||
const macTransparentWindow = isMac && windowStyle === 'transparent'
|
|
||||||
|
|
||||||
if (macTransparentWindow) {
|
|
||||||
return 'transparent'
|
return 'transparent'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
setLaunchToTray,
|
setLaunchToTray,
|
||||||
setPinTopicsToTop,
|
setPinTopicsToTop,
|
||||||
setSendMessageShortcut as _setSendMessageShortcut,
|
setSendMessageShortcut as _setSendMessageShortcut,
|
||||||
setShowTokens,
|
|
||||||
setSidebarIcons,
|
setSidebarIcons,
|
||||||
setTargetLanguage,
|
setTargetLanguage,
|
||||||
setTestChannel as _setTestChannel,
|
setTestChannel as _setTestChannel,
|
||||||
@@ -17,9 +16,9 @@ import {
|
|||||||
setTheme,
|
setTheme,
|
||||||
SettingsState,
|
SettingsState,
|
||||||
setTopicPosition,
|
setTopicPosition,
|
||||||
|
setTransparentWindow,
|
||||||
setTray as _setTray,
|
setTray as _setTray,
|
||||||
setTrayOnClose,
|
setTrayOnClose
|
||||||
setWindowStyle
|
|
||||||
} from '@renderer/store/settings'
|
} from '@renderer/store/settings'
|
||||||
import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
import { SidebarIcon, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
||||||
import { UpgradeChannel } from '@shared/config/constant'
|
import { UpgradeChannel } from '@shared/config/constant'
|
||||||
@@ -75,9 +74,6 @@ export function useSettings() {
|
|||||||
setTheme(theme: ThemeMode) {
|
setTheme(theme: ThemeMode) {
|
||||||
dispatch(setTheme(theme))
|
dispatch(setTheme(theme))
|
||||||
},
|
},
|
||||||
setWindowStyle(windowStyle: 'transparent' | 'opaque') {
|
|
||||||
dispatch(setWindowStyle(windowStyle))
|
|
||||||
},
|
|
||||||
setTargetLanguage(targetLanguage: TranslateLanguageVarious) {
|
setTargetLanguage(targetLanguage: TranslateLanguageVarious) {
|
||||||
dispatch(setTargetLanguage(targetLanguage))
|
dispatch(setTargetLanguage(targetLanguage))
|
||||||
},
|
},
|
||||||
@@ -99,8 +95,8 @@ export function useSettings() {
|
|||||||
setAssistantIconType(assistantIconType: AssistantIconType) {
|
setAssistantIconType(assistantIconType: AssistantIconType) {
|
||||||
dispatch(setAssistantIconType(assistantIconType))
|
dispatch(setAssistantIconType(assistantIconType))
|
||||||
},
|
},
|
||||||
setShowTokens(showTokens: boolean) {
|
setTransparentWindow(transparentWindow: boolean) {
|
||||||
dispatch(setShowTokens(showTokens))
|
dispatch(setTransparentWindow(transparentWindow))
|
||||||
},
|
},
|
||||||
setDisableHardwareAcceleration(disableHardwareAcceleration: boolean) {
|
setDisableHardwareAcceleration(disableHardwareAcceleration: boolean) {
|
||||||
dispatch(setDisableHardwareAcceleration(disableHardwareAcceleration))
|
dispatch(setDisableHardwareAcceleration(disableHardwareAcceleration))
|
||||||
|
|||||||
64
src/renderer/src/hooks/useTabs.ts
Normal file
64
src/renderer/src/hooks/useTabs.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
|
import type { Tab } from '@renderer/store/tabs'
|
||||||
|
import { addTab, removeTab, setActiveTab, updateTab } from '@renderer/store/tabs'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
export function useTabs() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
const tabs = useAppSelector((state) => state.tabs.tabs)
|
||||||
|
const activeTabId = useAppSelector((state) => state.tabs.activeTabId)
|
||||||
|
const activeTab = useAppSelector((state) => state.tabs.tabs.find((tab) => tab.id === activeTabId))
|
||||||
|
|
||||||
|
const getTabId = (path: string): string => {
|
||||||
|
if (path === '/') return 'home'
|
||||||
|
const segments = path.split('/')
|
||||||
|
return segments[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldCreateTab = (path: string) => {
|
||||||
|
if (path === '/') return false
|
||||||
|
return !tabs.some((tab) => tab.id === getTabId(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
const addNewTab = (tab: Tab) => {
|
||||||
|
dispatch(addTab(tab))
|
||||||
|
navigate(tab.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeTab = (tabId: string) => {
|
||||||
|
if (tabs.length === 1) return
|
||||||
|
|
||||||
|
if (tabId === activeTabId) {
|
||||||
|
const remainingTabs = tabs.filter((tab) => tab.id !== tabId)
|
||||||
|
const lastTab = remainingTabs[remainingTabs.length - 1]
|
||||||
|
navigate(lastTab.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(removeTab(tabId))
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchTab = (tabId: string) => {
|
||||||
|
const tab = tabs.find((tab) => tab.id === tabId)
|
||||||
|
if (tab) {
|
||||||
|
dispatch(setActiveTab(tabId))
|
||||||
|
navigate(tab.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCurrentTab = (updates: Partial<Tab>) => {
|
||||||
|
dispatch(updateTab({ id: activeTabId, updates }))
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
tabs,
|
||||||
|
activeTab,
|
||||||
|
activeTabId,
|
||||||
|
addNewTab,
|
||||||
|
closeTab,
|
||||||
|
switchTab,
|
||||||
|
getTabId,
|
||||||
|
shouldCreateTab,
|
||||||
|
updateCurrentTab
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ export const useTags = () => {
|
|||||||
|
|
||||||
// 计算所有标签
|
// 计算所有标签
|
||||||
const allTags = useMemo(() => {
|
const allTags = useMemo(() => {
|
||||||
const tags = uniq(flatMap(assistants, (assistant) => assistant.tags || []))
|
const tags = uniq(flatMap(assistants, (assistant) => assistant?.tags || []))
|
||||||
if (savedTagsOrder.length > 0) {
|
if (savedTagsOrder.length > 0) {
|
||||||
return [
|
return [
|
||||||
...savedTagsOrder.filter((tag) => tags.includes(tag)),
|
...savedTagsOrder.filter((tag) => tags.includes(tag)),
|
||||||
@@ -50,6 +50,7 @@ export const useTags = () => {
|
|||||||
|
|
||||||
// 按标签分组并构建结果
|
// 按标签分组并构建结果
|
||||||
const grouped = Object.entries(groupBy(assistantsByTags, 'tag')).map(([tag, group]) => ({
|
const grouped = Object.entries(groupBy(assistantsByTags, 'tag')).map(([tag, group]) => ({
|
||||||
|
id: tag,
|
||||||
tag,
|
tag,
|
||||||
assistants: group.map((g) => g.assistant)
|
assistants: group.map((g) => g.assistant)
|
||||||
}))
|
}))
|
||||||
|
|||||||
@@ -1,59 +1,23 @@
|
|||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
import { fetchMessagesSummary } from '@renderer/services/ApiService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|
||||||
import { deleteMessageFiles } from '@renderer/services/MessagesService'
|
import { deleteMessageFiles } from '@renderer/services/MessagesService'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { updateTopic } from '@renderer/store/assistants'
|
|
||||||
import { setNewlyRenamedTopics, setRenamingTopics } from '@renderer/store/runtime'
|
import { setNewlyRenamedTopics, setRenamingTopics } from '@renderer/store/runtime'
|
||||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||||
|
import { selectTopicById, topicsActions } from '@renderer/store/topics'
|
||||||
import { Assistant, Topic } from '@renderer/types'
|
import { Assistant, Topic } from '@renderer/types'
|
||||||
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
|
import { findMainTextBlocks } from '@renderer/utils/messageUtils/find'
|
||||||
import { find, isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
import { useAssistant } from './useAssistant'
|
|
||||||
import { getStoreSetting } from './useSettings'
|
import { getStoreSetting } from './useSettings'
|
||||||
|
|
||||||
let _activeTopic: Topic
|
export function getTopic(topicId: string) {
|
||||||
let _setActiveTopic: (topic: Topic) => void
|
return selectTopicById(store.getState(), topicId)
|
||||||
|
|
||||||
export function useActiveTopic(_assistant: Assistant, topic?: Topic) {
|
|
||||||
const { assistant } = useAssistant(_assistant.id)
|
|
||||||
const [activeTopic, setActiveTopic] = useState(topic || _activeTopic || assistant?.topics[0])
|
|
||||||
|
|
||||||
_activeTopic = activeTopic
|
|
||||||
_setActiveTopic = setActiveTopic
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (activeTopic) {
|
|
||||||
store.dispatch(loadTopicMessagesThunk(activeTopic.id))
|
|
||||||
EventEmitter.emit(EVENT_NAMES.CHANGE_TOPIC, activeTopic)
|
|
||||||
}
|
|
||||||
}, [activeTopic])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// activeTopic not in assistant.topics
|
|
||||||
if (assistant && !find(assistant.topics, { id: activeTopic?.id })) {
|
|
||||||
setActiveTopic(assistant.topics[0])
|
|
||||||
}
|
|
||||||
}, [activeTopic?.id, assistant])
|
|
||||||
|
|
||||||
return { activeTopic, setActiveTopic }
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTopic(assistant: Assistant, topicId?: string) {
|
|
||||||
return assistant?.topics.find((topic) => topic.id === topicId)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTopic(assistant: Assistant, topicId: string) {
|
|
||||||
return assistant?.topics.find((topic) => topic.id === topicId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTopicById(topicId: string) {
|
export async function getTopicById(topicId: string) {
|
||||||
const assistants = store.getState().assistants.assistants
|
const topic = selectTopicById(store.getState(), topicId)
|
||||||
const topics = assistants.map((assistant) => assistant.topics).flat()
|
|
||||||
const topic = topics.find((topic) => topic.id === topicId)
|
|
||||||
const messages = await TopicManager.getTopicMessages(topicId)
|
const messages = await TopicManager.getTopicMessages(topicId)
|
||||||
return { ...topic, messages } as Topic
|
return { ...topic, messages } as Topic
|
||||||
}
|
}
|
||||||
@@ -122,8 +86,7 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
|
|||||||
startTopicRenaming(topicId)
|
startTopicRenaming(topicId)
|
||||||
|
|
||||||
const data = { ...topic, name: topicName } as Topic
|
const data = { ...topic, name: topicName } as Topic
|
||||||
topic.id === _activeTopic.id && _setActiveTopic(data)
|
store.dispatch(topicsActions.updateTopic({ assistantId: assistant.id, topic: data }))
|
||||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
|
||||||
} finally {
|
} finally {
|
||||||
finishTopicRenaming(topicId)
|
finishTopicRenaming(topicId)
|
||||||
}
|
}
|
||||||
@@ -137,8 +100,7 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) =>
|
|||||||
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
|
const summaryText = await fetchMessagesSummary({ messages: topic.messages, assistant })
|
||||||
if (summaryText) {
|
if (summaryText) {
|
||||||
const data = { ...topic, name: summaryText }
|
const data = { ...topic, name: summaryText }
|
||||||
topic.id === _activeTopic.id && _setActiveTopic(data)
|
store.dispatch(topicsActions.updateTopic({ assistantId: assistant.id, topic: data }))
|
||||||
store.dispatch(updateTopic({ assistantId: assistant.id, topic: data }))
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
finishTopicRenaming(topicId)
|
finishTopicRenaming(topicId)
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"translation": {
|
||||||
|
"title": {
|
||||||
|
"home": "Home",
|
||||||
|
"agents": "Agents",
|
||||||
|
"paintings": "Paintings",
|
||||||
|
"translate": "Translate",
|
||||||
|
"files": "Files",
|
||||||
|
"knowledge": "Knowledge Base",
|
||||||
|
"apps": "Apps",
|
||||||
|
"mcp-servers": "MCP Servers",
|
||||||
|
"settings": "Settings",
|
||||||
|
"launchpad": "Launchpad"
|
||||||
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"add.button": "Add to Assistant",
|
"add.button": "Add to Assistant",
|
||||||
"add.knowledge_base": "Knowledge Base",
|
"add.knowledge_base": "Knowledge Base",
|
||||||
@@ -463,6 +475,7 @@
|
|||||||
"pinyin.asc": "Sort by Pinyin (A-Z)",
|
"pinyin.asc": "Sort by Pinyin (A-Z)",
|
||||||
"pinyin.desc": "Sort by Pinyin (Z-A)"
|
"pinyin.desc": "Sort by Pinyin (Z-A)"
|
||||||
},
|
},
|
||||||
|
"apps": "Apps",
|
||||||
"success": "Success",
|
"success": "Success",
|
||||||
"swap": "Swap",
|
"swap": "Swap",
|
||||||
"topics": "Topics",
|
"topics": "Topics",
|
||||||
@@ -522,7 +535,7 @@
|
|||||||
"count": "files",
|
"count": "files",
|
||||||
"created_at": "Created At",
|
"created_at": "Created At",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete this file?",
|
"delete.content": "Deleting a file will delete its reference from all messages. Are you sure you want to delete {{count}} files?",
|
||||||
"delete.paintings.warning": "Image contains this file, deletion is not possible",
|
"delete.paintings.warning": "Image contains this file, deletion is not possible",
|
||||||
"delete.title": "Delete File",
|
"delete.title": "Delete File",
|
||||||
"document": "Document",
|
"document": "Document",
|
||||||
@@ -534,7 +547,9 @@
|
|||||||
"size": "Size",
|
"size": "Size",
|
||||||
"text": "Text",
|
"text": "Text",
|
||||||
"title": "Files",
|
"title": "Files",
|
||||||
"type": "Type"
|
"type": "Type",
|
||||||
|
"batch_operation": "Batch Operation",
|
||||||
|
"batch_delete": "Batch Delete"
|
||||||
},
|
},
|
||||||
"gpustack": {
|
"gpustack": {
|
||||||
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
"keep_alive_time.description": "The time in minutes to keep the connection alive, default is 5 minutes.",
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"translation": {
|
||||||
|
"title": {
|
||||||
|
"home": "ホーム",
|
||||||
|
"agents": "エージェント",
|
||||||
|
"paintings": "ペインティング",
|
||||||
|
"translate": "翻訳",
|
||||||
|
"files": "ファイル",
|
||||||
|
"knowledge": "ナレッジベース",
|
||||||
|
"apps": "アプリ",
|
||||||
|
"mcp-servers": "MCP サーバー",
|
||||||
|
"settings": "設定",
|
||||||
|
"launchpad": "ランチパッド"
|
||||||
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"add.button": "アシスタントに追加",
|
"add.button": "アシスタントに追加",
|
||||||
"add.knowledge_base": "ナレッジベース",
|
"add.knowledge_base": "ナレッジベース",
|
||||||
@@ -463,6 +475,7 @@
|
|||||||
"pinyin.asc": "ピンインで昇順ソート",
|
"pinyin.asc": "ピンインで昇順ソート",
|
||||||
"pinyin.desc": "ピンインで降順ソート"
|
"pinyin.desc": "ピンインで降順ソート"
|
||||||
},
|
},
|
||||||
|
"apps": "アプリ",
|
||||||
"success": "成功",
|
"success": "成功",
|
||||||
"swap": "交換",
|
"swap": "交換",
|
||||||
"topics": "トピック",
|
"topics": "トピック",
|
||||||
@@ -522,7 +535,7 @@
|
|||||||
"count": "ファイル",
|
"count": "ファイル",
|
||||||
"created_at": "作成日",
|
"created_at": "作成日",
|
||||||
"delete": "削除",
|
"delete": "削除",
|
||||||
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。このファイルを削除してもよろしいですか?",
|
"delete.content": "ファイルを削除すると、ファイルがすべてのメッセージで参照されることを削除します。この{{count}}ファイルを削除してもよろしいですか?",
|
||||||
"delete.paintings.warning": "画像に含まれているため、削除できません",
|
"delete.paintings.warning": "画像に含まれているため、削除できません",
|
||||||
"delete.title": "ファイルを削除",
|
"delete.title": "ファイルを削除",
|
||||||
"document": "ドキュメント",
|
"document": "ドキュメント",
|
||||||
@@ -534,7 +547,9 @@
|
|||||||
"size": "サイズ",
|
"size": "サイズ",
|
||||||
"text": "テキスト",
|
"text": "テキスト",
|
||||||
"title": "ファイル",
|
"title": "ファイル",
|
||||||
"type": "タイプ"
|
"type": "タイプ",
|
||||||
|
"batch_operation": "一括操作",
|
||||||
|
"batch_delete": "一括削除"
|
||||||
},
|
},
|
||||||
"gpustack": {
|
"gpustack": {
|
||||||
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
|
"keep_alive_time.description": "モデルがメモリに保持される時間(デフォルト:5分)",
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"translation": {
|
||||||
|
"title": {
|
||||||
|
"home": "Главная",
|
||||||
|
"agents": "Агенты",
|
||||||
|
"paintings": "Рисунки",
|
||||||
|
"translate": "Перевод",
|
||||||
|
"files": "Файлы",
|
||||||
|
"knowledge": "База знаний",
|
||||||
|
"apps": "Приложения",
|
||||||
|
"mcp-servers": "MCP серверы",
|
||||||
|
"settings": "Настройки",
|
||||||
|
"launchpad": "Запуск"
|
||||||
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"add.button": "Добавить в ассистента",
|
"add.button": "Добавить в ассистента",
|
||||||
"add.knowledge_base": "База знаний",
|
"add.knowledge_base": "База знаний",
|
||||||
@@ -467,7 +479,8 @@
|
|||||||
"swap": "Поменять местами",
|
"swap": "Поменять местами",
|
||||||
"topics": "Топики",
|
"topics": "Топики",
|
||||||
"warning": "Предупреждение",
|
"warning": "Предупреждение",
|
||||||
"you": "Вы"
|
"you": "Вы",
|
||||||
|
"apps": "Приложения"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "Документация"
|
"title": "Документация"
|
||||||
@@ -522,7 +535,7 @@
|
|||||||
"count": "файлов",
|
"count": "файлов",
|
||||||
"created_at": "Дата создания",
|
"created_at": "Дата создания",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот файл?",
|
"delete.content": "Удаление файла удалит его из всех сообщений, вы уверены, что хотите удалить этот {{count}} файл",
|
||||||
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно",
|
"delete.paintings.warning": "В изображениях содержится этот файл, удаление невозможно",
|
||||||
"delete.title": "Удалить файл",
|
"delete.title": "Удалить файл",
|
||||||
"document": "Документ",
|
"document": "Документ",
|
||||||
@@ -534,7 +547,9 @@
|
|||||||
"size": "Размер",
|
"size": "Размер",
|
||||||
"text": "Текст",
|
"text": "Текст",
|
||||||
"title": "Файлы",
|
"title": "Файлы",
|
||||||
"type": "Тип"
|
"type": "Тип",
|
||||||
|
"batch_operation": "Пакетная операция",
|
||||||
|
"batch_delete": "Пакетное удаление"
|
||||||
},
|
},
|
||||||
"gpustack": {
|
"gpustack": {
|
||||||
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
"keep_alive_time.description": "Время в минутах, в течение которого модель остается активной, по умолчанию 5 минут.",
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"translation": {
|
||||||
|
"title": {
|
||||||
|
"home": "首页",
|
||||||
|
"agents": "智能体",
|
||||||
|
"paintings": "绘画",
|
||||||
|
"translate": "翻译",
|
||||||
|
"files": "文件",
|
||||||
|
"knowledge": "知识库",
|
||||||
|
"apps": "小程序",
|
||||||
|
"mcp-servers": "MCP 服务器",
|
||||||
|
"settings": "设置",
|
||||||
|
"launchpad": "启动台"
|
||||||
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"add.button": "添加到助手",
|
"add.button": "添加到助手",
|
||||||
"add.knowledge_base": "知识库",
|
"add.knowledge_base": "知识库",
|
||||||
@@ -467,7 +479,8 @@
|
|||||||
"swap": "交换",
|
"swap": "交换",
|
||||||
"topics": "话题",
|
"topics": "话题",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"you": "用户"
|
"you": "用户",
|
||||||
|
"apps": "应用"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "帮助文档"
|
"title": "帮助文档"
|
||||||
@@ -522,7 +535,7 @@
|
|||||||
"count": "个文件",
|
"count": "个文件",
|
||||||
"created_at": "创建时间",
|
"created_at": "创建时间",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除此文件吗?",
|
"delete.content": "删除文件会删除文件在所有消息中的引用,确定要删除这{{count}}个文件吗?",
|
||||||
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除",
|
"delete.paintings.warning": "绘图中包含该图片,暂时无法删除",
|
||||||
"delete.title": "删除文件",
|
"delete.title": "删除文件",
|
||||||
"document": "文档",
|
"document": "文档",
|
||||||
@@ -534,7 +547,9 @@
|
|||||||
"size": "大小",
|
"size": "大小",
|
||||||
"text": "文本",
|
"text": "文本",
|
||||||
"title": "文件",
|
"title": "文件",
|
||||||
"type": "类型"
|
"type": "类型",
|
||||||
|
"batch_operation": "批量操作",
|
||||||
|
"batch_delete": "批量删除"
|
||||||
},
|
},
|
||||||
"gpustack": {
|
"gpustack": {
|
||||||
"keep_alive_time.description": "模型在内存中保持的时间(默认:5 分钟)",
|
"keep_alive_time.description": "模型在内存中保持的时间(默认:5 分钟)",
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
{
|
{
|
||||||
"translation": {
|
"translation": {
|
||||||
|
"title": {
|
||||||
|
"home": "主頁",
|
||||||
|
"agents": "智能體",
|
||||||
|
"paintings": "繪畫",
|
||||||
|
"translate": "翻譯",
|
||||||
|
"files": "文件",
|
||||||
|
"knowledge": "知識庫",
|
||||||
|
"apps": "小程序",
|
||||||
|
"mcp-servers": "MCP 伺服器",
|
||||||
|
"settings": "設定",
|
||||||
|
"launchpad": "啟動台"
|
||||||
|
},
|
||||||
"agents": {
|
"agents": {
|
||||||
"add.button": "新增到助手",
|
"add.button": "新增到助手",
|
||||||
"add.knowledge_base": "知識庫",
|
"add.knowledge_base": "知識庫",
|
||||||
@@ -467,7 +479,8 @@
|
|||||||
"swap": "交換",
|
"swap": "交換",
|
||||||
"topics": "話題",
|
"topics": "話題",
|
||||||
"warning": "警告",
|
"warning": "警告",
|
||||||
"you": "您"
|
"you": "您",
|
||||||
|
"apps": "應用"
|
||||||
},
|
},
|
||||||
"docs": {
|
"docs": {
|
||||||
"title": "說明文件"
|
"title": "說明文件"
|
||||||
@@ -522,7 +535,7 @@
|
|||||||
"count": "個檔案",
|
"count": "個檔案",
|
||||||
"created_at": "建立時間",
|
"created_at": "建立時間",
|
||||||
"delete": "刪除",
|
"delete": "刪除",
|
||||||
"delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除此檔案嗎?",
|
"delete.content": "刪除檔案會刪除檔案在所有訊息中的引用,確定要刪除這{{count}}個檔案嗎?",
|
||||||
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除",
|
"delete.paintings.warning": "繪圖中包含該圖片,暫時無法刪除",
|
||||||
"delete.title": "刪除檔案",
|
"delete.title": "刪除檔案",
|
||||||
"document": "文件",
|
"document": "文件",
|
||||||
@@ -534,7 +547,9 @@
|
|||||||
"size": "大小",
|
"size": "大小",
|
||||||
"text": "文字",
|
"text": "文字",
|
||||||
"title": "檔案",
|
"title": "檔案",
|
||||||
"type": "類型"
|
"type": "類型",
|
||||||
|
"batch_operation": "批量操作",
|
||||||
|
"batch_delete": "批量刪除"
|
||||||
},
|
},
|
||||||
"gpustack": {
|
"gpustack": {
|
||||||
"keep_alive_time.description": "模型在記憶體中保持的時間(預設為 5 分鐘)",
|
"keep_alive_time.description": "模型在記憶體中保持的時間(預設為 5 分鐘)",
|
||||||
@@ -2448,4 +2463,4 @@
|
|||||||
"visualization": "視覺化"
|
"visualization": "視覺化"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1575,7 +1575,6 @@
|
|||||||
"theme.light": "Φωτεινό",
|
"theme.light": "Φωτεινό",
|
||||||
"theme.system": "Σύστημα",
|
"theme.system": "Σύστημα",
|
||||||
"theme.title": "Θέμα",
|
"theme.title": "Θέμα",
|
||||||
"theme.window.style.opaque": "Μη διαφανή παράθυρα",
|
|
||||||
"theme.window.style.title": "Στυλ παραθύρων",
|
"theme.window.style.title": "Στυλ παραθύρων",
|
||||||
"theme.window.style.transparent": "Διαφανή παράθυρα",
|
"theme.window.style.transparent": "Διαφανή παράθυρα",
|
||||||
"title": "Ρυθμίσεις",
|
"title": "Ρυθμίσεις",
|
||||||
|
|||||||
@@ -1574,7 +1574,6 @@
|
|||||||
"theme.light": "Claro",
|
"theme.light": "Claro",
|
||||||
"theme.system": "Sistema",
|
"theme.system": "Sistema",
|
||||||
"theme.title": "Tema",
|
"theme.title": "Tema",
|
||||||
"theme.window.style.opaque": "Ventana opaca",
|
|
||||||
"theme.window.style.title": "Estilo de ventana",
|
"theme.window.style.title": "Estilo de ventana",
|
||||||
"theme.window.style.transparent": "Ventana transparente",
|
"theme.window.style.transparent": "Ventana transparente",
|
||||||
"title": "Configuración",
|
"title": "Configuración",
|
||||||
|
|||||||
@@ -1575,7 +1575,6 @@
|
|||||||
"theme.light": "Clair",
|
"theme.light": "Clair",
|
||||||
"theme.system": "Système",
|
"theme.system": "Système",
|
||||||
"theme.title": "Thème",
|
"theme.title": "Thème",
|
||||||
"theme.window.style.opaque": "Fenêtre opaque",
|
|
||||||
"theme.window.style.title": "Style de fenêtre",
|
"theme.window.style.title": "Style de fenêtre",
|
||||||
"theme.window.style.transparent": "Fenêtre transparente",
|
"theme.window.style.transparent": "Fenêtre transparente",
|
||||||
"title": "Paramètres",
|
"title": "Paramètres",
|
||||||
|
|||||||
@@ -1576,7 +1576,6 @@
|
|||||||
"theme.light": "Claro",
|
"theme.light": "Claro",
|
||||||
"theme.system": "Sistema",
|
"theme.system": "Sistema",
|
||||||
"theme.title": "Tema",
|
"theme.title": "Tema",
|
||||||
"theme.window.style.opaque": "Janela opaca",
|
|
||||||
"theme.window.style.title": "Estilo de janela",
|
"theme.window.style.title": "Estilo de janela",
|
||||||
"theme.window.style.transparent": "Janela transparente",
|
"theme.window.style.transparent": "Janela transparente",
|
||||||
"title": "Configurações",
|
"title": "Configurações",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
|
import { ImportOutlined, PlusOutlined } from '@ant-design/icons'
|
||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
|
||||||
import CustomTag from '@renderer/components/CustomTag'
|
import CustomTag from '@renderer/components/CustomTag'
|
||||||
import ListItem from '@renderer/components/ListItem'
|
import ListItem from '@renderer/components/ListItem'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { useAgents } from '@renderer/hooks/useAgents'
|
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||||
import { Agent } from '@renderer/types'
|
import { Agent } from '@renderer/types'
|
||||||
import { uuid } from '@renderer/utils'
|
import { uuid } from '@renderer/utils'
|
||||||
@@ -28,7 +28,7 @@ const AgentsPage: FC = () => {
|
|||||||
const [activeGroup, setActiveGroup] = useState('我的')
|
const [activeGroup, setActiveGroup] = useState('我的')
|
||||||
const [agentGroups, setAgentGroups] = useState<Record<string, Agent[]>>({})
|
const [agentGroups, setAgentGroups] = useState<Record<string, Agent[]>>({})
|
||||||
const systemAgents = useSystemAgents()
|
const systemAgents = useSystemAgents()
|
||||||
const { agents: userAgents } = useAgents()
|
const { templates: userAgents } = useAssistants()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const systemAgentsGroupList = groupByCategories(systemAgents)
|
const systemAgentsGroupList = groupByCategories(systemAgents)
|
||||||
@@ -152,7 +152,7 @@ const AgentsPage: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Navbar>
|
<NavbarMain>
|
||||||
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
<NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}>
|
||||||
{t('agents.title')}
|
{t('agents.title')}
|
||||||
<Input
|
<Input
|
||||||
@@ -169,9 +169,9 @@ const AgentsPage: FC = () => {
|
|||||||
onChange={(e) => setSearchInput(e.target.value)}
|
onChange={(e) => setSearchInput(e.target.value)}
|
||||||
onPressEnter={handleSearch}
|
onPressEnter={handleSearch}
|
||||||
/>
|
/>
|
||||||
<div style={{ width: 80 }} />
|
<div style={{ width: 1 }} />
|
||||||
</NavbarCenter>
|
</NavbarCenter>
|
||||||
</Navbar>
|
</NavbarMain>
|
||||||
|
|
||||||
<Main id="content-container">
|
<Main id="content-container">
|
||||||
<AgentsGroupList>
|
<AgentsGroupList>
|
||||||
@@ -321,7 +321,7 @@ const AgentDescription = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const AgentPrompt = styled.div`
|
const AgentPrompt = styled.div`
|
||||||
max-height: 60vh;
|
max-height: 50vh;
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
background-color: var(--color-background-soft);
|
background-color: var(--color-background-soft);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { CheckOutlined, LoadingOutlined, RollbackOutlined, ThunderboltOutlined }
|
|||||||
import EmojiPicker from '@renderer/components/EmojiPicker'
|
import EmojiPicker from '@renderer/components/EmojiPicker'
|
||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { AGENT_PROMPT } from '@renderer/config/prompts'
|
import { AGENT_PROMPT } from '@renderer/config/prompts'
|
||||||
import { useAgents } from '@renderer/hooks/useAgents'
|
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
||||||
import { fetchGenerate } from '@renderer/services/ApiService'
|
import { fetchGenerate } from '@renderer/services/ApiService'
|
||||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||||
@@ -14,6 +14,7 @@ import { Agent, KnowledgeBase } from '@renderer/types'
|
|||||||
import { getLeadingEmoji, uuid } from '@renderer/utils'
|
import { getLeadingEmoji, uuid } from '@renderer/utils'
|
||||||
import { Button, Form, FormInstance, Input, Modal, Popover, Select, SelectProps } from 'antd'
|
import { Button, Form, FormInstance, Input, Modal, Popover, Select, SelectProps } from 'antd'
|
||||||
import TextArea from 'antd/es/input/TextArea'
|
import TextArea from 'antd/es/input/TextArea'
|
||||||
|
import { ChevronDown } from 'lucide-react'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import stringWidth from 'string-width'
|
import stringWidth from 'string-width'
|
||||||
@@ -34,7 +35,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { addAgent } = useAgents()
|
const { addTemplate } = useAssistants()
|
||||||
const formRef = useRef<FormInstance>(null)
|
const formRef = useRef<FormInstance>(null)
|
||||||
const [emoji, setEmoji] = useState('')
|
const [emoji, setEmoji] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -83,12 +84,12 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
emoji: _emoji,
|
emoji: _emoji,
|
||||||
prompt: values.prompt,
|
prompt: values.prompt,
|
||||||
defaultModel: getDefaultModel(),
|
defaultModel: getDefaultModel(),
|
||||||
type: 'agent',
|
|
||||||
topics: [],
|
topics: [],
|
||||||
messages: []
|
messages: [],
|
||||||
|
isTemplate: true
|
||||||
}
|
}
|
||||||
|
|
||||||
addAgent(_agent)
|
addTemplate(_agent)
|
||||||
resolve(_agent)
|
resolve(_agent)
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
}
|
}
|
||||||
@@ -239,6 +240,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.includes(input.toLowerCase())
|
.includes(input.toLowerCase())
|
||||||
}
|
}
|
||||||
|
suffixIcon={<ChevronDown size={16} color="var(--color-border)" />}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
SortAscendingOutlined
|
SortAscendingOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import CustomTag from '@renderer/components/CustomTag'
|
import CustomTag from '@renderer/components/CustomTag'
|
||||||
import { useAgents } from '@renderer/hooks/useAgents'
|
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
import AssistantSettingsPopup from '@renderer/pages/settings/AssistantSettings'
|
||||||
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
import { createAssistantFromAgent } from '@renderer/services/AssistantService'
|
||||||
import type { Agent } from '@renderer/types'
|
import type { Agent } from '@renderer/types'
|
||||||
@@ -27,7 +27,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AgentCard: FC<Props> = ({ agent, onClick, activegroup, getLocalizedGroupName }) => {
|
const AgentCard: FC<Props> = ({ agent, onClick, activegroup, getLocalizedGroupName }) => {
|
||||||
const { removeAgent } = useAgents()
|
const { removeAssistant: removeAgent } = useAssistants()
|
||||||
const [isVisible, setIsVisible] = useState(false)
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
const cardRef = useRef<HTMLDivElement>(null)
|
const cardRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { useAgents } from '@renderer/hooks/useAgents'
|
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import { getDefaultModel } from '@renderer/services/AssistantService'
|
import { getDefaultModel } from '@renderer/services/AssistantService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { Agent } from '@renderer/types'
|
import { Agent } from '@renderer/types'
|
||||||
@@ -16,7 +16,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
|||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { addAgent } = useAgents()
|
const { addTemplate: addAgent } = useAssistants()
|
||||||
const [importType, setImportType] = useState<'url' | 'file'>('url')
|
const [importType, setImportType] = useState<'url' | 'file'>('url')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { MenuOutlined } from '@ant-design/icons'
|
|||||||
import { DraggableList } from '@renderer/components/DraggableList'
|
import { DraggableList } from '@renderer/components/DraggableList'
|
||||||
import { Box, HStack } from '@renderer/components/Layout'
|
import { Box, HStack } from '@renderer/components/Layout'
|
||||||
import { TopView } from '@renderer/components/TopView'
|
import { TopView } from '@renderer/components/TopView'
|
||||||
import { useAgents } from '@renderer/hooks/useAgents'
|
import { useAssistants } from '@renderer/hooks/useAssistant'
|
||||||
import { Empty, Modal } from 'antd'
|
import { Empty, Modal } from 'antd'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -11,7 +11,8 @@ import styled from 'styled-components'
|
|||||||
const PopupContainer: React.FC = () => {
|
const PopupContainer: React.FC = () => {
|
||||||
const [open, setOpen] = useState(true)
|
const [open, setOpen] = useState(true)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { agents, updateAgents } = useAgents()
|
const { assistants, updateAssistants: updateAgents } = useAssistants()
|
||||||
|
const agents = assistants.filter((a) => a.isTemplate)
|
||||||
|
|
||||||
const onOk = () => {
|
const onOk = () => {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Navbar, NavbarMain } from '@renderer/components/app/Navbar'
|
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
|
||||||
import { useMinapps } from '@renderer/hooks/useMinapps'
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
import { Button, Input } from 'antd'
|
import { Button, Input } from 'antd'
|
||||||
import { Search, SettingsIcon, X } from 'lucide-react'
|
import { Search, SettingsIcon } from 'lucide-react'
|
||||||
import React, { FC, useEffect, useState } from 'react'
|
import React, { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useLocation } from 'react-router'
|
import { useLocation } from 'react-router'
|
||||||
@@ -41,8 +41,8 @@ const AppsPage: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container onContextMenu={handleContextMenu}>
|
<Container onContextMenu={handleContextMenu}>
|
||||||
<Navbar>
|
<NavbarMain>
|
||||||
<NavbarMain>
|
<NavbarCenter>
|
||||||
{t('minapp.title')}
|
{t('minapp.title')}
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('common.search')}
|
placeholder={t('common.search')}
|
||||||
@@ -50,10 +50,7 @@ const AppsPage: FC = () => {
|
|||||||
style={{
|
style={{
|
||||||
width: '30%',
|
width: '30%',
|
||||||
height: 28,
|
height: 28,
|
||||||
borderRadius: 15,
|
borderRadius: 15
|
||||||
position: 'absolute',
|
|
||||||
left: '50vw',
|
|
||||||
transform: 'translateX(-50%)'
|
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
@@ -65,11 +62,11 @@ const AppsPage: FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
className="nodrag"
|
className="nodrag"
|
||||||
icon={isSettingsOpen ? <X size={18} /> : <SettingsIcon size={18} color="var(--color-text-2)" />}
|
icon={<SettingsIcon size={18} color={isSettingsOpen ? 'var(--color-primary)' : 'var(--color-text-2)'} />}
|
||||||
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||||
/>
|
/>
|
||||||
</NavbarMain>
|
</NavbarCenter>
|
||||||
</Navbar>
|
</NavbarMain>
|
||||||
<ContentContainer id="content-container">
|
<ContentContainer id="content-container">
|
||||||
{isSettingsOpen && <MiniAppSettings />}
|
{isSettingsOpen && <MiniAppSettings />}
|
||||||
{!isSettingsOpen && (
|
{!isSettingsOpen && (
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
|
|
||||||
import { handleDelete } from '@renderer/services/FileAction'
|
|
||||||
import FileManager from '@renderer/services/FileManager'
|
|
||||||
import { FileMetadata, FileTypes } from '@renderer/types'
|
import { FileMetadata, FileTypes } from '@renderer/types'
|
||||||
import { formatFileSize } from '@renderer/utils'
|
|
||||||
import { Col, Image, Row, Spin } from 'antd'
|
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import VirtualList from 'rc-virtual-list'
|
import VirtualList from 'rc-virtual-list'
|
||||||
import React, { memo } from 'react'
|
import React, { memo } from 'react'
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
import FileItem from './FileItem'
|
import FileItem from './FileItem'
|
||||||
|
import ImageList from './ImageList'
|
||||||
|
|
||||||
interface FileItemProps {
|
interface FileListProps {
|
||||||
id: FileTypes | 'all' | string
|
id: FileTypes | 'all' | string
|
||||||
list: {
|
list: {
|
||||||
key: FileTypes | 'all' | string
|
key: FileTypes | 'all' | string
|
||||||
@@ -26,55 +21,9 @@ interface FileItemProps {
|
|||||||
files?: FileMetadata[]
|
files?: FileMetadata[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
|
const FileList: React.FC<FileListProps> = ({ id, list, files }) => {
|
||||||
if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
|
if (id === FileTypes.IMAGE && files?.length && files?.length > 0) {
|
||||||
return (
|
return <ImageList files={files}></ImageList>
|
||||||
<div style={{ padding: 16, overflowY: 'auto' }}>
|
|
||||||
<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.size)}</div>
|
|
||||||
</ImageInfo>
|
|
||||||
<DeleteButton
|
|
||||||
title={t('files.delete.title')}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
window.modal.confirm({
|
|
||||||
title: t('files.delete.title'),
|
|
||||||
content: t('files.delete.content'),
|
|
||||||
okText: t('common.confirm'),
|
|
||||||
cancelText: t('common.cancel'),
|
|
||||||
centered: true,
|
|
||||||
onOk: () => {
|
|
||||||
handleDelete(file.id, t)
|
|
||||||
},
|
|
||||||
icon: <ExclamationCircleOutlined style={{ color: 'red' }} />
|
|
||||||
})
|
|
||||||
}}>
|
|
||||||
<DeleteOutlined />
|
|
||||||
</DeleteButton>
|
|
||||||
</ImageWrapper>
|
|
||||||
</Col>
|
|
||||||
))}
|
|
||||||
</Row>
|
|
||||||
</Image.PreviewGroup>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -113,92 +62,4 @@ const FileList: React.FC<FileItemProps> = ({ id, list, files }) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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 DeleteButton = styled.div`
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
right: 8px;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(0, 0, 0, 0.6);
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: rgba(255, 0, 0, 0.8);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default memo(FileList)
|
export default memo(FileList)
|
||||||
|
|||||||
@@ -5,18 +5,19 @@ import {
|
|||||||
SortAscendingOutlined,
|
SortAscendingOutlined,
|
||||||
SortDescendingOutlined
|
SortDescendingOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Navbar, NavbarCenter } from '@renderer/components/app/Navbar'
|
import { NavbarCenter, NavbarMain } from '@renderer/components/app/Navbar'
|
||||||
import ListItem from '@renderer/components/ListItem'
|
import ListItem from '@renderer/components/ListItem'
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { handleDelete, handleRename, sortFiles, tempFilesSort } from '@renderer/services/FileAction'
|
import { handleDelete, handleRename, sortFiles, tempFilesSort } from '@renderer/services/FileAction'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
|
import store from '@renderer/store'
|
||||||
import { FileMetadata, FileTypes } from '@renderer/types'
|
import { FileMetadata, FileTypes } from '@renderer/types'
|
||||||
import { formatFileSize } from '@renderer/utils'
|
import { formatFileSize } from '@renderer/utils'
|
||||||
import { Button, Empty, Flex, Popconfirm } from 'antd'
|
import { Button, Checkbox, Dropdown, Empty, Flex, Popconfirm } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { File as FileIcon, FileImage, FileText, FileType as FileTypeIcon } from 'lucide-react'
|
import { File as FileIcon, FileImage, FileText, FileType as FileTypeIcon } from 'lucide-react'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@@ -30,6 +31,11 @@ const FilesPage: FC = () => {
|
|||||||
const [fileType, setFileType] = useState<string>('document')
|
const [fileType, setFileType] = useState<string>('document')
|
||||||
const [sortField, setSortField] = useState<SortField>('created_at')
|
const [sortField, setSortField] = useState<SortField>('created_at')
|
||||||
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
|
const [sortOrder, setSortOrder] = useState<SortOrder>('desc')
|
||||||
|
const [selectedFileIds, setSelectedFileIds] = useState<string[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSelectedFileIds([])
|
||||||
|
}, [fileType])
|
||||||
|
|
||||||
const files = useLiveQuery<FileMetadata[]>(() => {
|
const files = useLiveQuery<FileMetadata[]>(() => {
|
||||||
if (fileType === 'all') {
|
if (fileType === 'all') {
|
||||||
@@ -40,6 +46,46 @@ const FilesPage: FC = () => {
|
|||||||
|
|
||||||
const sortedFiles = files ? sortFiles(files, sortField, sortOrder) : []
|
const sortedFiles = files ? sortFiles(files, sortField, sortOrder) : []
|
||||||
|
|
||||||
|
const handleBatchDelete = async () => {
|
||||||
|
const selectedFiles = await Promise.all(selectedFileIds.map((id) => FileManager.getFile(id)))
|
||||||
|
const validFiles = selectedFiles.filter((file): file is FileType => file !== null && file !== undefined)
|
||||||
|
|
||||||
|
const paintings = await store.getState().paintings.paintings
|
||||||
|
const paintingsFiles = paintings.flatMap((p) => p.files)
|
||||||
|
|
||||||
|
const filesInPaintings = validFiles.filter((file) => paintingsFiles.some((p) => p.id === file.id))
|
||||||
|
|
||||||
|
if (filesInPaintings.length > 0) {
|
||||||
|
window.modal.warning({
|
||||||
|
content: t('files.delete.paintings.warning', { count: filesInPaintings.length }),
|
||||||
|
centered: true
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const fileId of selectedFileIds) {
|
||||||
|
await handleDelete(fileId, t, setSelectedFileIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedFileIds([])
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectFile = (fileId: string, checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedFileIds((prev) => [...prev, fileId])
|
||||||
|
} else {
|
||||||
|
setSelectedFileIds((prev) => prev.filter((id) => id !== fileId))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelectAll = (checked: boolean) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedFileIds(sortedFiles.map((file) => file.id))
|
||||||
|
} else {
|
||||||
|
setSelectedFileIds([])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dataSource = sortedFiles?.map((file) => {
|
const dataSource = sortedFiles?.map((file) => {
|
||||||
return {
|
return {
|
||||||
key: file.id,
|
key: file.id,
|
||||||
@@ -56,13 +102,20 @@ const FilesPage: FC = () => {
|
|||||||
<Button type="text" icon={<EditOutlined />} onClick={() => handleRename(file.id)} />
|
<Button type="text" icon={<EditOutlined />} onClick={() => handleRename(file.id)} />
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t('files.delete.title')}
|
title={t('files.delete.title')}
|
||||||
description={t('files.delete.content')}
|
description={t('files.delete.content', { count: 1 })}
|
||||||
okText={t('common.confirm')}
|
okText={t('common.confirm')}
|
||||||
cancelText={t('common.cancel')}
|
cancelText={t('common.cancel')}
|
||||||
onConfirm={() => handleDelete(file.id, t)}
|
onConfirm={() => handleDelete(file.id, t, setSelectedFileIds)}
|
||||||
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
|
icon={<ExclamationCircleOutlined style={{ color: 'red' }} />}>
|
||||||
<Button type="text" danger icon={<DeleteOutlined />} />
|
<Button type="text" danger icon={<DeleteOutlined />} />
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
|
{fileType !== 'image' && (
|
||||||
|
<Checkbox
|
||||||
|
checked={selectedFileIds.includes(file.id)}
|
||||||
|
onChange={(e) => handleSelectFile(file.id, e.target.checked)}
|
||||||
|
style={{ margin: '0 8px' }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -77,10 +130,72 @@ const FilesPage: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Navbar>
|
<NavbarMain>
|
||||||
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
|
<NavbarCenter style={{ borderRight: 'none' }}>{t('files.title')}</NavbarCenter>
|
||||||
</Navbar>
|
</NavbarMain>
|
||||||
<ContentContainer id="content-container">
|
<ContentContainer id="content-container">
|
||||||
|
<MainContent>
|
||||||
|
<SortContainer>
|
||||||
|
<Flex gap={8} align="center">
|
||||||
|
{['created_at', 'size', 'name'].map((field) => (
|
||||||
|
<Button
|
||||||
|
color="default"
|
||||||
|
key={field}
|
||||||
|
variant={sortField === field ? 'filled' : 'text'}
|
||||||
|
onClick={() => {
|
||||||
|
if (sortField === field) {
|
||||||
|
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
||||||
|
} else {
|
||||||
|
setSortField(field as 'created_at' | 'size' | 'name')
|
||||||
|
setSortOrder('desc')
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{t(`files.${field}`)}
|
||||||
|
{sortField === field &&
|
||||||
|
(sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
{fileType !== 'image' && (
|
||||||
|
<Dropdown.Button
|
||||||
|
style={{ width: 'auto' }}
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
disabled: selectedFileIds.length === 0,
|
||||||
|
danger: true,
|
||||||
|
label: (
|
||||||
|
<Popconfirm
|
||||||
|
disabled={selectedFileIds.length === 0}
|
||||||
|
title={t('files.delete.title')}
|
||||||
|
description={t('files.delete.content', { count: selectedFileIds.length })}
|
||||||
|
okText={t('common.confirm')}
|
||||||
|
cancelText={t('common.cancel')}
|
||||||
|
onConfirm={handleBatchDelete}
|
||||||
|
icon={<ExclamationCircleOutlined />}>
|
||||||
|
{t('files.batch_delete')} ({selectedFileIds.length})
|
||||||
|
</Popconfirm>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
trigger={['click']}>
|
||||||
|
<Checkbox
|
||||||
|
indeterminate={selectedFileIds.length > 0 && selectedFileIds.length < sortedFiles.length}
|
||||||
|
checked={selectedFileIds.length === sortedFiles.length}
|
||||||
|
onChange={(e) => handleSelectAll(e.target.checked)}>
|
||||||
|
{t('files.batch_operation')}
|
||||||
|
</Checkbox>
|
||||||
|
</Dropdown.Button>
|
||||||
|
)}
|
||||||
|
</SortContainer>
|
||||||
|
{dataSource && dataSource?.length > 0 ? (
|
||||||
|
<FileList id={fileType} list={dataSource} files={sortedFiles} />
|
||||||
|
) : (
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
)}
|
||||||
|
</MainContent>
|
||||||
<SideNav>
|
<SideNav>
|
||||||
{menuItems.map((item) => (
|
{menuItems.map((item) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
@@ -92,31 +207,6 @@ const FilesPage: FC = () => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SideNav>
|
</SideNav>
|
||||||
<MainContent>
|
|
||||||
<SortContainer>
|
|
||||||
{['created_at', 'size', 'name'].map((field) => (
|
|
||||||
<SortButton
|
|
||||||
key={field}
|
|
||||||
active={sortField === field}
|
|
||||||
onClick={() => {
|
|
||||||
if (sortField === field) {
|
|
||||||
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')
|
|
||||||
} else {
|
|
||||||
setSortField(field as 'created_at' | 'size' | 'name')
|
|
||||||
setSortOrder('desc')
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{t(`files.${field}`)}
|
|
||||||
{sortField === field && (sortOrder === 'desc' ? <SortDescendingOutlined /> : <SortAscendingOutlined />)}
|
|
||||||
</SortButton>
|
|
||||||
))}
|
|
||||||
</SortContainer>
|
|
||||||
{dataSource && dataSource?.length > 0 ? (
|
|
||||||
<FileList id={fileType} list={dataSource} files={sortedFiles} />
|
|
||||||
) : (
|
|
||||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
||||||
)}
|
|
||||||
</MainContent>
|
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
@@ -138,6 +228,7 @@ const MainContent = styled.div`
|
|||||||
const SortContainer = styled.div`
|
const SortContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-bottom: 0.5px solid var(--color-border);
|
border-bottom: 0.5px solid var(--color-border);
|
||||||
@@ -184,25 +275,4 @@ const SideNav = styled.div`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const SortButton = styled(Button)<{ active?: boolean }>`
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
padding: 4px 12px;
|
|
||||||
height: 30px;
|
|
||||||
border-radius: var(--list-item-border-radius);
|
|
||||||
border: 0.5px solid ${(props) => (props.active ? 'var(--color-border)' : 'transparent')};
|
|
||||||
background-color: ${(props) => (props.active ? 'var(--color-background-soft)' : 'transparent')};
|
|
||||||
color: ${(props) => (props.active ? 'var(--color-text)' : 'var(--color-text-secondary)')};
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--color-background-soft);
|
|
||||||
color: var(--color-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.anticon {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
export default FilesPage
|
export default FilesPage
|
||||||
|
|||||||
149
src/renderer/src/pages/files/ImageItem.tsx
Normal file
149
src/renderer/src/pages/files/ImageItem.tsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { DeleteOutlined, ExclamationCircleOutlined } from '@ant-design/icons'
|
||||||
|
import { handleDelete } from '@renderer/services/FileAction'
|
||||||
|
import FileManager from '@renderer/services/FileManager'
|
||||||
|
import { FileType } from '@renderer/types'
|
||||||
|
import { formatFileSize } from '@renderer/utils'
|
||||||
|
import { Col, Image, Spin } from 'antd'
|
||||||
|
import { memo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface ImageItemProps {
|
||||||
|
file: FileType
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageItem: React.FC<ImageItemProps> = ({ file }) => {
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
return (
|
||||||
|
<Col xs={24} sm={12} md={8} lg={4} xl={3}>
|
||||||
|
<ImageWrapper>
|
||||||
|
{loading && (
|
||||||
|
<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')
|
||||||
|
setLoading(false)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ImageInfo>
|
||||||
|
<div>{formatFileSize(file.size)}</div>
|
||||||
|
</ImageInfo>
|
||||||
|
<DeleteButton
|
||||||
|
title={t('files.delete.title')}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
window.modal.confirm({
|
||||||
|
title: t('files.delete.title'),
|
||||||
|
content: t('files.delete.content'),
|
||||||
|
okText: t('common.confirm'),
|
||||||
|
cancelText: t('common.cancel'),
|
||||||
|
centered: true,
|
||||||
|
onOk: () => {
|
||||||
|
handleDelete(file.id, t)
|
||||||
|
},
|
||||||
|
icon: <ExclamationCircleOutlined style={{ color: 'red' }} />
|
||||||
|
})
|
||||||
|
}}>
|
||||||
|
<DeleteOutlined />
|
||||||
|
</DeleteButton>
|
||||||
|
</ImageWrapper>
|
||||||
|
</Col>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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 DeleteButton = styled.div`
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(255, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default memo(ImageItem)
|
||||||
21
src/renderer/src/pages/files/ImageList.tsx
Normal file
21
src/renderer/src/pages/files/ImageList.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { FileType } from '@renderer/types'
|
||||||
|
import { Image, Row } from 'antd'
|
||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
|
import ImageItem from './ImageItem'
|
||||||
|
|
||||||
|
interface ImageListProps {
|
||||||
|
files?: FileType[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImageList: React.FC<ImageListProps> = ({ files }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: 16, overflowY: 'auto' }}>
|
||||||
|
<Image.PreviewGroup>
|
||||||
|
<Row gutter={[16, 16]}>{files?.map((file) => <ImageItem key={file.id} file={file}></ImageItem>)}</Row>
|
||||||
|
</Image.PreviewGroup>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(ImageList)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import { ChatProvider } from '@renderer/hooks/useChat'
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||||
import { Topic } from '@renderer/types'
|
import { Topic } from '@renderer/types'
|
||||||
@@ -72,51 +73,53 @@ const TopicsPage: FC = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<ChatProvider>
|
||||||
<HStack style={{ padding: '0 12px', marginTop: 8 }}>
|
<Container>
|
||||||
<Input
|
<HStack style={{ padding: '0 12px', marginTop: 8 }}>
|
||||||
prefix={
|
<Input
|
||||||
stack.length > 1 ? (
|
prefix={
|
||||||
<SearchIcon className="back-icon" onClick={goBack}>
|
stack.length > 1 ? (
|
||||||
<ChevronLeft size={16} />
|
<SearchIcon className="back-icon" onClick={goBack}>
|
||||||
</SearchIcon>
|
<ChevronLeft size={16} />
|
||||||
) : (
|
</SearchIcon>
|
||||||
<SearchIcon>
|
) : (
|
||||||
<Search size={15} />
|
<SearchIcon>
|
||||||
</SearchIcon>
|
<Search size={15} />
|
||||||
)
|
</SearchIcon>
|
||||||
}
|
)
|
||||||
suffix={search.length >= 2 ? <CornerDownLeft size={16} /> : null}
|
}
|
||||||
ref={inputRef}
|
suffix={search.length >= 2 ? <CornerDownLeft size={16} /> : null}
|
||||||
placeholder={t('history.search.placeholder')}
|
ref={inputRef}
|
||||||
value={search}
|
placeholder={t('history.search.placeholder')}
|
||||||
onChange={(e) => setSearch(e.target.value.trimStart())}
|
value={search}
|
||||||
allowClear
|
onChange={(e) => setSearch(e.target.value.trimStart())}
|
||||||
autoFocus
|
allowClear
|
||||||
spellCheck={false}
|
autoFocus
|
||||||
style={{ paddingLeft: 0 }}
|
spellCheck={false}
|
||||||
variant="borderless"
|
style={{ paddingLeft: 0 }}
|
||||||
size="middle"
|
variant="borderless"
|
||||||
onPressEnter={onSearch}
|
size="middle"
|
||||||
/>
|
onPressEnter={onSearch}
|
||||||
</HStack>
|
/>
|
||||||
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
|
</HStack>
|
||||||
|
<Divider style={{ margin: 0, marginTop: 4, borderBlockStartWidth: 0.5 }} />
|
||||||
|
|
||||||
<TopicsHistory
|
<TopicsHistory
|
||||||
keywords={search}
|
keywords={search}
|
||||||
onClick={onTopicClick as any}
|
onClick={onTopicClick as any}
|
||||||
onSearch={onSearch}
|
onSearch={onSearch}
|
||||||
style={{ display: isShow('topics') }}
|
style={{ display: isShow('topics') }}
|
||||||
/>
|
/>
|
||||||
<TopicMessages topic={topic} style={{ display: isShow('topic') }} />
|
<TopicMessages topic={topic} style={{ display: isShow('topic') }} />
|
||||||
<SearchResults
|
<SearchResults
|
||||||
keywords={isShow('search') ? searchKeywords : ''}
|
keywords={isShow('search') ? searchKeywords : ''}
|
||||||
onMessageClick={onMessageClick}
|
onMessageClick={onMessageClick}
|
||||||
onTopicClick={onTopicClick}
|
onTopicClick={onTopicClick}
|
||||||
style={{ display: isShow('search') }}
|
style={{ display: isShow('search') }}
|
||||||
/>
|
/>
|
||||||
<SearchMessage message={message} style={{ display: isShow('message') }} />
|
<SearchMessage message={message} style={{ display: isShow('message') }} />
|
||||||
</Container>
|
</Container>
|
||||||
|
</ChatProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
|||||||
import { getTopicById } from '@renderer/hooks/useTopic'
|
import { getTopicById } from '@renderer/hooks/useTopic'
|
||||||
import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
|
import { default as MessageItem } from '@renderer/pages/home/Messages/Message'
|
||||||
import { locateToMessage } from '@renderer/services/MessagesService'
|
import { locateToMessage } from '@renderer/services/MessagesService'
|
||||||
import NavigationService from '@renderer/services/NavigationService'
|
|
||||||
import { Topic } from '@renderer/types'
|
import { Topic } from '@renderer/types'
|
||||||
import type { Message } from '@renderer/types/newMessage'
|
import type { Message } from '@renderer/types/newMessage'
|
||||||
import { runAsyncFunction } from '@renderer/utils'
|
import { runAsyncFunction } from '@renderer/utils'
|
||||||
@@ -18,7 +17,6 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
||||||
const navigate = NavigationService.navigate!
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [topic, setTopic] = useState<Topic | null>(null)
|
const [topic, setTopic] = useState<Topic | null>(null)
|
||||||
|
|
||||||
@@ -48,11 +46,11 @@ const SearchMessage: FC<Props> = ({ message, ...props }) => {
|
|||||||
type="text"
|
type="text"
|
||||||
size="middle"
|
size="middle"
|
||||||
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 16, top: 16 }}
|
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 16, top: 16 }}
|
||||||
onClick={() => locateToMessage(navigate, message)}
|
onClick={() => locateToMessage(message)}
|
||||||
icon={<Forward size={16} />}
|
icon={<Forward size={16} />}
|
||||||
/>
|
/>
|
||||||
<HStack mt="10px" justifyContent="center">
|
<HStack mt="10px" justifyContent="center">
|
||||||
<Button onClick={() => locateToMessage(navigate, message)} icon={<Forward size={16} />}>
|
<Button onClick={() => locateToMessage(message)} icon={<Forward size={16} />}>
|
||||||
{t('history.locate.message')}
|
{t('history.locate.message')}
|
||||||
</Button>
|
</Button>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import { MessageOutlined } from '@ant-design/icons'
|
|||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||||
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
||||||
|
import { useChat } from '@renderer/hooks/useChat'
|
||||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||||
import { getAssistantById } from '@renderer/services/AssistantService'
|
import { getAssistantById } from '@renderer/services/AssistantService'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { isGenerating, locateToMessage } from '@renderer/services/MessagesService'
|
import { locateToMessage } from '@renderer/services/MessagesService'
|
||||||
import NavigationService from '@renderer/services/NavigationService'
|
|
||||||
import { useAppDispatch } from '@renderer/store'
|
import { useAppDispatch } from '@renderer/store'
|
||||||
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk'
|
||||||
import { Topic } from '@renderer/types'
|
import { Topic } from '@renderer/types'
|
||||||
@@ -23,9 +23,9 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
||||||
const navigate = NavigationService.navigate!
|
|
||||||
const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
|
const { handleScroll, containerRef } = useScrollPosition('TopicMessages')
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { setActiveAssistant, setActiveTopic } = useChat()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
topic && dispatch(loadTopicMessagesThunk(topic.id))
|
topic && dispatch(loadTopicMessagesThunk(topic.id))
|
||||||
@@ -38,11 +38,13 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onContinueChat = async (topic: Topic) => {
|
const onContinueChat = async (topic: Topic) => {
|
||||||
await isGenerating()
|
|
||||||
SearchPopup.hide()
|
SearchPopup.hide()
|
||||||
const assistant = getAssistantById(topic.assistantId)
|
const assistant = getAssistantById(topic.assistantId)
|
||||||
navigate('/', { state: { assistant, topic } })
|
if (assistant) {
|
||||||
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100)
|
setActiveAssistant(assistant)
|
||||||
|
setActiveTopic(topic)
|
||||||
|
setTimeout(() => EventEmitter.emit(EVENT_NAMES.SHOW_TOPIC_SIDEBAR), 100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,7 +58,7 @@ const TopicMessages: FC<Props> = ({ topic, ...props }) => {
|
|||||||
type="text"
|
type="text"
|
||||||
size="middle"
|
size="middle"
|
||||||
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }}
|
style={{ color: 'var(--color-text-3)', position: 'absolute', right: 0, top: 5 }}
|
||||||
onClick={() => locateToMessage(navigate, message)}
|
onClick={() => locateToMessage(message)}
|
||||||
icon={<Forward size={16} />}
|
icon={<Forward size={16} />}
|
||||||
/>
|
/>
|
||||||
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" />
|
<Divider style={{ margin: '8px auto 15px' }} variant="dashed" />
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { SearchOutlined } from '@ant-design/icons'
|
import { SearchOutlined } from '@ant-design/icons'
|
||||||
import { VStack } from '@renderer/components/Layout'
|
import { VStack } from '@renderer/components/Layout'
|
||||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
|
||||||
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
import useScrollPosition from '@renderer/hooks/useScrollPosition'
|
||||||
import { getTopicById } from '@renderer/hooks/useTopic'
|
import { getTopicById } from '@renderer/hooks/useTopic'
|
||||||
|
import { useAppSelector } from '@renderer/store'
|
||||||
|
import { selectActiveAssistants } from '@renderer/store/assistants'
|
||||||
import { Topic } from '@renderer/types'
|
import { Topic } from '@renderer/types'
|
||||||
import { Button, Divider, Empty, Segmented } from 'antd'
|
import { Button, Divider, Empty, Segmented } from 'antd'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
@@ -20,7 +21,7 @@ type Props = {
|
|||||||
} & React.HTMLAttributes<HTMLDivElement>
|
} & React.HTMLAttributes<HTMLDivElement>
|
||||||
|
|
||||||
const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props }) => {
|
const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props }) => {
|
||||||
const { assistants } = useAssistants()
|
const assistants = useAppSelector(selectActiveAssistants)
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { handleScroll, containerRef } = useScrollPosition('TopicsHistory')
|
const { handleScroll, containerRef } = useScrollPosition('TopicsHistory')
|
||||||
const [sortType, setSortType] = useState<SortType>('createdAt')
|
const [sortType, setSortType] = useState<SortType>('createdAt')
|
||||||
@@ -35,6 +36,15 @@ const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props
|
|||||||
return dayjs(topic[sortType]).format('MM/DD')
|
return dayjs(topic[sortType]).format('MM/DD')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 创建助手映射表
|
||||||
|
const assistantMap = assistants.reduce(
|
||||||
|
(map, assistant) => {
|
||||||
|
map[assistant.id] = assistant
|
||||||
|
return map
|
||||||
|
},
|
||||||
|
{} as Record<string, any>
|
||||||
|
)
|
||||||
|
|
||||||
if (isEmpty(filteredTopics)) {
|
if (isEmpty(filteredTopics)) {
|
||||||
return (
|
return (
|
||||||
<ListContainer {...props}>
|
<ListContainer {...props}>
|
||||||
@@ -65,17 +75,27 @@ const TopicsHistory: React.FC<Props> = ({ keywords, onClick, onSearch, ...props
|
|||||||
<ListItem key={date}>
|
<ListItem key={date}>
|
||||||
<Date>{date}</Date>
|
<Date>{date}</Date>
|
||||||
<Divider style={{ margin: '5px 0' }} />
|
<Divider style={{ margin: '5px 0' }} />
|
||||||
{items.map((topic) => (
|
{items.map((topic) => {
|
||||||
<TopicItem
|
const assistant = assistantMap[topic.assistantId]
|
||||||
key={topic.id}
|
return (
|
||||||
onClick={async () => {
|
<TopicItem
|
||||||
const _topic = await getTopicById(topic.id)
|
key={topic.id}
|
||||||
onClick(_topic)
|
onClick={async () => {
|
||||||
}}>
|
const _topic = await getTopicById(topic.id)
|
||||||
<TopicName>{topic.name.substring(0, 50)}</TopicName>
|
onClick(_topic)
|
||||||
<TopicDate>{dayjs(topic[sortType]).format('HH:mm')}</TopicDate>
|
}}>
|
||||||
</TopicItem>
|
<TopicContent>
|
||||||
))}
|
<TopicName>{topic.name.substring(0, 50)}</TopicName>
|
||||||
|
{assistant && (
|
||||||
|
<AssistantTag>
|
||||||
|
{assistant.emoji} {assistant.name}
|
||||||
|
</AssistantTag>
|
||||||
|
)}
|
||||||
|
</TopicContent>
|
||||||
|
<TopicDate>{dayjs(topic[sortType]).format('HH:mm')}</TopicDate>
|
||||||
|
</TopicItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
{keywords.length >= 2 && (
|
{keywords.length >= 2 && (
|
||||||
@@ -127,7 +147,15 @@ const TopicItem = styled.div`
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 30px;
|
min-height: 30px;
|
||||||
|
padding: 4px 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TopicContent = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 1;
|
||||||
`
|
`
|
||||||
|
|
||||||
const TopicName = styled.div`
|
const TopicName = styled.div`
|
||||||
@@ -135,10 +163,21 @@ const TopicName = styled.div`
|
|||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const AssistantTag = styled.div`
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
background: var(--color-fill-quaternary);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
width: fit-content;
|
||||||
|
`
|
||||||
|
|
||||||
const TopicDate = styled.div`
|
const TopicDate = styled.div`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--color-text-3);
|
color: var(--color-text-3);
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
flex-shrink: 0;
|
||||||
`
|
`
|
||||||
|
|
||||||
export default TopicsHistory
|
export default TopicsHistory
|
||||||
|
|||||||
@@ -1,47 +1,29 @@
|
|||||||
import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch'
|
import { ContentSearch, ContentSearchRef } from '@renderer/components/ContentSearch'
|
||||||
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
|
import MultiSelectActionPopup from '@renderer/components/Popups/MultiSelectionPopup'
|
||||||
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
|
import { QuickPanelProvider } from '@renderer/components/QuickPanel'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useChat } from '@renderer/hooks/useChat'
|
||||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||||
import { useShowTopics } from '@renderer/hooks/useStore'
|
|
||||||
import { Assistant, Topic } from '@renderer/types'
|
|
||||||
import { classNames } from '@renderer/utils'
|
import { classNames } from '@renderer/utils'
|
||||||
import { Flex } from 'antd'
|
import { Flex } from 'antd'
|
||||||
import { debounce } from 'lodash'
|
import { debounce } from 'lodash'
|
||||||
import React, { FC, useMemo, useState } from 'react'
|
import React, { FC, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import Inputbar from './Inputbar/Inputbar'
|
import Inputbar from './Inputbar/Inputbar'
|
||||||
import Messages from './Messages/Messages'
|
import Messages from './Messages/Messages'
|
||||||
import Tabs from './Tabs'
|
|
||||||
|
|
||||||
interface Props {
|
const Chat: FC = () => {
|
||||||
assistant: Assistant
|
const { activeAssistant, activeTopic, setActiveTopic } = useChat()
|
||||||
activeTopic: Topic
|
const { messageStyle } = useSettings()
|
||||||
setActiveTopic: (topic: Topic) => void
|
const { isMultiSelectMode } = useChatContext(activeTopic)
|
||||||
setActiveAssistant: (assistant: Assistant) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const Chat: FC<Props> = (props) => {
|
|
||||||
const { assistant } = useAssistant(props.assistant.id)
|
|
||||||
const { topicPosition, messageStyle, showAssistants } = useSettings()
|
|
||||||
const { showTopics } = useShowTopics()
|
|
||||||
const { isMultiSelectMode } = useChatContext(props.activeTopic)
|
|
||||||
|
|
||||||
const mainRef = React.useRef<HTMLDivElement>(null)
|
const mainRef = React.useRef<HTMLDivElement>(null)
|
||||||
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
const contentSearchRef = React.useRef<ContentSearchRef>(null)
|
||||||
const [filterIncludeUser, setFilterIncludeUser] = useState(false)
|
const [filterIncludeUser, setFilterIncludeUser] = useState(false)
|
||||||
|
|
||||||
const maxWidth = useMemo(() => {
|
|
||||||
const showRightTopics = showTopics && topicPosition === 'right'
|
|
||||||
const minusAssistantsWidth = showAssistants ? '- var(--assistants-width)' : ''
|
|
||||||
const minusRightTopicsWidth = showRightTopics ? '- var(--assistants-width)' : ''
|
|
||||||
return `calc(100vw - var(--sidebar-width) ${minusAssistantsWidth} ${minusRightTopicsWidth})`
|
|
||||||
}, [showAssistants, showTopics, topicPosition])
|
|
||||||
|
|
||||||
useHotkeys('esc', () => {
|
useHotkeys('esc', () => {
|
||||||
contentSearchRef.current?.disable()
|
contentSearchRef.current?.disable()
|
||||||
})
|
})
|
||||||
@@ -100,48 +82,36 @@ const Chat: FC<Props> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container id="chat" className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}>
|
<Main
|
||||||
<Main ref={mainRef} id="chat-main" vertical flex={1} justify="space-between" style={{ maxWidth }}>
|
ref={mainRef}
|
||||||
<Messages
|
id="chat-main"
|
||||||
key={props.activeTopic.id}
|
className={classNames([messageStyle, { 'multi-select-mode': isMultiSelectMode }])}
|
||||||
assistant={assistant}
|
vertical
|
||||||
topic={props.activeTopic}
|
flex={1}
|
||||||
setActiveTopic={props.setActiveTopic}
|
justify="space-between">
|
||||||
onComponentUpdate={messagesComponentUpdateHandler}
|
<Messages
|
||||||
onFirstUpdate={messagesComponentFirstUpdateHandler}
|
key={activeTopic.id}
|
||||||
/>
|
assistant={activeAssistant}
|
||||||
<ContentSearch
|
topic={activeTopic}
|
||||||
ref={contentSearchRef}
|
setActiveTopic={setActiveTopic}
|
||||||
searchTarget={mainRef as React.RefObject<HTMLElement>}
|
onComponentUpdate={messagesComponentUpdateHandler}
|
||||||
filter={contentSearchFilter}
|
onFirstUpdate={messagesComponentFirstUpdateHandler}
|
||||||
includeUser={filterIncludeUser}
|
/>
|
||||||
onIncludeUserChange={userOutlinedItemClickHandler}
|
<ContentSearch
|
||||||
/>
|
ref={contentSearchRef}
|
||||||
<QuickPanelProvider>
|
searchTarget={mainRef as React.RefObject<HTMLElement>}
|
||||||
<Inputbar assistant={assistant} setActiveTopic={props.setActiveTopic} topic={props.activeTopic} />
|
filter={contentSearchFilter}
|
||||||
{isMultiSelectMode && <MultiSelectActionPopup topic={props.activeTopic} />}
|
includeUser={filterIncludeUser}
|
||||||
</QuickPanelProvider>
|
onIncludeUserChange={userOutlinedItemClickHandler}
|
||||||
</Main>
|
/>
|
||||||
{topicPosition === 'right' && showTopics && (
|
<QuickPanelProvider>
|
||||||
<Tabs
|
<Inputbar />
|
||||||
activeAssistant={assistant}
|
{isMultiSelectMode && <MultiSelectActionPopup topic={activeTopic} />}
|
||||||
activeTopic={props.activeTopic}
|
</QuickPanelProvider>
|
||||||
setActiveAssistant={props.setActiveAssistant}
|
</Main>
|
||||||
setActiveTopic={props.setActiveTopic}
|
|
||||||
position="right"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Container>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
height: 100%;
|
|
||||||
flex: 1;
|
|
||||||
`
|
|
||||||
|
|
||||||
const Main = styled(Flex)`
|
const Main = styled(Flex)`
|
||||||
height: calc(100vh - var(--navbar-height));
|
height: calc(100vh - var(--navbar-height));
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
|
|||||||
91
src/renderer/src/pages/home/ChatNavbar.tsx
Normal file
91
src/renderer/src/pages/home/ChatNavbar.tsx
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { NavbarMain } from '@renderer/components/app/Navbar'
|
||||||
|
import { HStack } from '@renderer/components/Layout'
|
||||||
|
import SearchPopup from '@renderer/components/Popups/SearchPopup'
|
||||||
|
import { isMac } from '@renderer/config/constant'
|
||||||
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
|
import { useChat } from '@renderer/hooks/useChat'
|
||||||
|
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||||
|
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
|
import { t } from 'i18next'
|
||||||
|
import { PanelLeft, PanelRight, Search } from 'lucide-react'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import SelectModelButton from './components/SelectModelButton'
|
||||||
|
import UpdateAppButton from './components/UpdateAppButton'
|
||||||
|
|
||||||
|
const ChatNavbar: FC = () => {
|
||||||
|
const { activeAssistant } = useChat()
|
||||||
|
const { assistant } = useAssistant(activeAssistant.id)
|
||||||
|
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||||
|
|
||||||
|
useShortcut('search_message', SearchPopup.show)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavbarMain className="home-navbar" style={{ minHeight: 50 }}>
|
||||||
|
<HStack alignItems="center" gap={8}>
|
||||||
|
<NavbarIcon onClick={() => toggleShowAssistants()}>
|
||||||
|
{showAssistants ? <PanelLeft size={18} /> : <PanelRight size={18} />}
|
||||||
|
</NavbarIcon>
|
||||||
|
<SelectModelButton assistant={assistant} />
|
||||||
|
</HStack>
|
||||||
|
<HStack alignItems="center" gap={8}>
|
||||||
|
<UpdateAppButton />
|
||||||
|
{isMac && (
|
||||||
|
<Tooltip title={t('chat.assistant.search.placeholder')} mouseEnterDelay={0.8}>
|
||||||
|
<NarrowIcon onClick={() => SearchPopup.show()}>
|
||||||
|
<Search size={18} />
|
||||||
|
</NarrowIcon>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</NavbarMain>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NavbarIcon = styled.div`
|
||||||
|
-webkit-app-region: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 7px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
cursor: pointer;
|
||||||
|
.iconfont {
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--color-icon);
|
||||||
|
&.icon-a-addchat {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
&.icon-a-darkmode {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
&.icon-appstore {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.anticon {
|
||||||
|
color: var(--color-icon);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
color: var(--color-icon-white);
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-background-mute);
|
||||||
|
color: var(--color-icon-white);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const NarrowIcon = styled(NavbarIcon)`
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default ChatNavbar
|
||||||
@@ -1,58 +1,15 @@
|
|||||||
import { useAssistants } from '@renderer/hooks/useAssistant'
|
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useActiveTopic } from '@renderer/hooks/useTopic'
|
import { FC, useEffect } from 'react'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|
||||||
import NavigationService from '@renderer/services/NavigationService'
|
|
||||||
import { Assistant } from '@renderer/types'
|
|
||||||
import { FC, useEffect, useState } from 'react'
|
|
||||||
import { useLocation, useNavigate } from 'react-router-dom'
|
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
import Chat from './Chat'
|
import Chat from './Chat'
|
||||||
import Navbar from './Navbar'
|
import ChatNavbar from './ChatNavbar'
|
||||||
import HomeTabs from './Tabs'
|
|
||||||
|
|
||||||
let _activeAssistant: Assistant
|
const HomePage: FC<{ style?: React.CSSProperties }> = ({ style }) => {
|
||||||
|
|
||||||
const HomePage: FC = () => {
|
|
||||||
const { assistants } = useAssistants()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
|
|
||||||
const location = useLocation()
|
|
||||||
const state = location.state
|
|
||||||
|
|
||||||
const [activeAssistant, setActiveAssistant] = useState(state?.assistant || _activeAssistant || assistants[0])
|
|
||||||
const { activeTopic, setActiveTopic } = useActiveTopic(activeAssistant, state?.topic)
|
|
||||||
const { showAssistants, showTopics, topicPosition } = useSettings()
|
const { showAssistants, showTopics, topicPosition } = useSettings()
|
||||||
|
|
||||||
_activeAssistant = activeAssistant
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
NavigationService.setNavigate(navigate)
|
window.api.window.setMinimumSize(showAssistants ? 1080 : 520, 600)
|
||||||
}, [navigate])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
state?.assistant && setActiveAssistant(state?.assistant)
|
|
||||||
state?.topic && setActiveTopic(state?.topic)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [state])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const unsubscribe = EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => {
|
|
||||||
const newAssistant = assistants.find((a) => a.id === assistantId)
|
|
||||||
if (newAssistant) {
|
|
||||||
setActiveAssistant(newAssistant)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unsubscribe()
|
|
||||||
}
|
|
||||||
}, [assistants, setActiveAssistant])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const canMinimize = topicPosition == 'left' ? !showAssistants : !showAssistants && !showTopics
|
|
||||||
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.api.window.resetMinimumSize()
|
window.api.window.resetMinimumSize()
|
||||||
@@ -60,47 +17,22 @@ const HomePage: FC = () => {
|
|||||||
}, [showAssistants, showTopics, topicPosition])
|
}, [showAssistants, showTopics, topicPosition])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container id="home-page">
|
<Container style={style}>
|
||||||
<Navbar
|
<ChatNavbar />
|
||||||
activeAssistant={activeAssistant}
|
|
||||||
activeTopic={activeTopic}
|
|
||||||
setActiveTopic={setActiveTopic}
|
|
||||||
setActiveAssistant={setActiveAssistant}
|
|
||||||
position="left"
|
|
||||||
/>
|
|
||||||
<ContentContainer id="content-container">
|
<ContentContainer id="content-container">
|
||||||
{showAssistants && (
|
<Chat />
|
||||||
<HomeTabs
|
|
||||||
activeAssistant={activeAssistant}
|
|
||||||
activeTopic={activeTopic}
|
|
||||||
setActiveAssistant={setActiveAssistant}
|
|
||||||
setActiveTopic={setActiveTopic}
|
|
||||||
position="left"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Chat
|
|
||||||
assistant={activeAssistant}
|
|
||||||
activeTopic={activeTopic}
|
|
||||||
setActiveTopic={setActiveTopic}
|
|
||||||
setActiveAssistant={setActiveAssistant}
|
|
||||||
/>
|
|
||||||
</ContentContainer>
|
</ContentContainer>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-width: calc(100vw - var(--sidebar-width));
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const ContentContainer = styled.div`
|
const ContentContainer = styled.div``
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
flex-direction: row;
|
|
||||||
overflow: hidden;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default HomePage
|
export default HomePage
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const GenerateImageButton: FC<Props> = ({ model, ToolbarButton, assistant, onEna
|
|||||||
mouseLeaveDelay={0}
|
mouseLeaveDelay={0}
|
||||||
arrow>
|
arrow>
|
||||||
<ToolbarButton type="text" disabled={!isGenerateImageModel(model)} onClick={onEnableGenerateImage}>
|
<ToolbarButton type="text" disabled={!isGenerateImageModel(model)} onClick={onEnableGenerateImage}>
|
||||||
<Image size={18} color={assistant.enableGenerateImage ? 'var(--color-link)' : 'var(--color-icon)'} />
|
<Image size={18} color={assistant.enableGenerateImage ? 'var(--color-primary)' : 'var(--color-icon)'} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import {
|
|||||||
} from '@renderer/config/models'
|
} from '@renderer/config/models'
|
||||||
import db from '@renderer/databases'
|
import db from '@renderer/databases'
|
||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
|
import { useChat } from '@renderer/hooks/useChat'
|
||||||
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
import { useKnowledgeBases } from '@renderer/hooks/useKnowledge'
|
||||||
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
|
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
|
||||||
import { modelGenerating, useRuntime } from '@renderer/hooks/useRuntime'
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
import { useShortcut, useShortcutDisplay } from '@renderer/hooks/useShortcuts'
|
||||||
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
import { useSidebarIconShow } from '@renderer/hooks/useSidebarIcon'
|
||||||
@@ -32,7 +33,7 @@ import WebSearchService from '@renderer/services/WebSearchService'
|
|||||||
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
import { useAppDispatch, useAppSelector } from '@renderer/store'
|
||||||
import { setSearching } from '@renderer/store/runtime'
|
import { setSearching } from '@renderer/store/runtime'
|
||||||
import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
|
import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk'
|
||||||
import { Assistant, FileType, FileTypes, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types'
|
import { FileType, FileTypes, KnowledgeBase, KnowledgeItem, Model } from '@renderer/types'
|
||||||
import type { MessageInputBaseParams } from '@renderer/types/newMessage'
|
import type { MessageInputBaseParams } from '@renderer/types/newMessage'
|
||||||
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
|
import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils'
|
||||||
import { formatQuotedText } from '@renderer/utils/formats'
|
import { formatQuotedText } from '@renderer/utils/formats'
|
||||||
@@ -55,21 +56,17 @@ import InputbarTools, { InputbarToolsRef } from './InputbarTools'
|
|||||||
import KnowledgeBaseInput from './KnowledgeBaseInput'
|
import KnowledgeBaseInput from './KnowledgeBaseInput'
|
||||||
import MentionModelsInput from './MentionModelsInput'
|
import MentionModelsInput from './MentionModelsInput'
|
||||||
import SendMessageButton from './SendMessageButton'
|
import SendMessageButton from './SendMessageButton'
|
||||||
|
import SettingButton from './SettingButton'
|
||||||
import TokenCount from './TokenCount'
|
import TokenCount from './TokenCount'
|
||||||
|
|
||||||
interface Props {
|
|
||||||
assistant: Assistant
|
|
||||||
setActiveTopic: (topic: Topic) => void
|
|
||||||
topic: Topic
|
|
||||||
}
|
|
||||||
|
|
||||||
let _text = ''
|
let _text = ''
|
||||||
let _files: FileType[] = []
|
let _files: FileType[] = []
|
||||||
|
|
||||||
const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) => {
|
const Inputbar: FC = () => {
|
||||||
|
const { activeAssistant, activeTopic: topic, setActiveTopic } = useChat()
|
||||||
const [text, setText] = useState(_text)
|
const [text, setText] = useState(_text)
|
||||||
const [inputFocus, setInputFocus] = useState(false)
|
const [inputFocus, setInputFocus] = useState(false)
|
||||||
const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(_assistant.id)
|
const { assistant, addTopic, model, setModel, updateAssistant } = useAssistant(activeAssistant.id)
|
||||||
const {
|
const {
|
||||||
targetLanguage,
|
targetLanguage,
|
||||||
sendMessageShortcut,
|
sendMessageShortcut,
|
||||||
@@ -432,8 +429,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
}
|
}
|
||||||
|
|
||||||
const addNewTopic = useCallback(async () => {
|
const addNewTopic = useCallback(async () => {
|
||||||
await modelGenerating()
|
|
||||||
|
|
||||||
const topic = getDefaultTopic(assistant.id)
|
const topic = getDefaultTopic(assistant.id)
|
||||||
|
|
||||||
await db.topics.add({ id: topic.id, messages: [] })
|
await db.topics.add({ id: topic.id, messages: [] })
|
||||||
@@ -662,11 +657,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
|
const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true })
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
// EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => {
|
|
||||||
// setText(message.content)
|
|
||||||
// textareaRef.current?.focus()
|
|
||||||
// setTimeout(() => resizeTextArea(), 0)
|
|
||||||
// }),
|
|
||||||
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, ({ tokensCount, contextCount }) => {
|
EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, ({ tokensCount, contextCount }) => {
|
||||||
_setEstimateTokenCount(tokensCount)
|
_setEstimateTokenCount(tokensCount)
|
||||||
setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值
|
setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值
|
||||||
@@ -791,12 +781,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
} else {
|
} else {
|
||||||
textArea.style.height = 'auto'
|
textArea.style.height = 'auto'
|
||||||
setTextareaHeight(undefined)
|
setTextareaHeight(undefined)
|
||||||
requestAnimationFrame(() => {
|
setTimeout(() => resizeTextArea(true), 0)
|
||||||
if (textArea) {
|
|
||||||
const contentHeight = textArea.scrollHeight
|
|
||||||
textArea.style.height = contentHeight > 400 ? '400px' : `${contentHeight}px`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
textareaRef.current?.focus()
|
textareaRef.current?.focus()
|
||||||
@@ -907,11 +892,12 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
|||||||
ToolbarButton={ToolbarButton}
|
ToolbarButton={ToolbarButton}
|
||||||
onClick={onNewContext}
|
onClick={onNewContext}
|
||||||
/>
|
/>
|
||||||
|
<SettingButton assistant={assistant} ToolbarButton={ToolbarButton} />
|
||||||
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
||||||
{loading && (
|
{loading && (
|
||||||
<Tooltip placement="top" title={t('chat.input.pause')} mouseLeaveDelay={0} arrow>
|
<Tooltip placement="top" title={t('chat.input.pause')} mouseEnterDelay={0} arrow>
|
||||||
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2, marginTop: 1 }}>
|
<ToolbarButton type="text" onClick={onPause} style={{ width: 30, height: 30, marginRight: 2 }}>
|
||||||
<CirclePause style={{ color: 'var(--color-error)', fontSize: 20 }} />
|
<CirclePause style={{ color: 'var(--color-error)' }} size={28} />
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
@@ -959,9 +945,10 @@ const Container = styled.div`
|
|||||||
`
|
`
|
||||||
|
|
||||||
const InputBarContainer = styled.div`
|
const InputBarContainer = styled.div`
|
||||||
border: 0.5px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin: 16px 20px;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding-top: 8px; // 为拖动手柄留出空间
|
padding-top: 8px; // 为拖动手柄留出空间
|
||||||
background-color: var(--color-background-opacity);
|
background-color: var(--color-background-opacity);
|
||||||
@@ -1001,6 +988,9 @@ const Textarea = styled(TextArea)`
|
|||||||
&.ant-input {
|
&.ant-input {
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
.ant-input-textarea-show-count::after {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 3px;
|
width: 3px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ const MCPToolsButton: FC<Props> = ({ ref, setInputValue, resizeTextArea, Toolbar
|
|||||||
newList.push({
|
newList.push({
|
||||||
label: t('settings.mcp.addServer') + '...',
|
label: t('settings.mcp.addServer') + '...',
|
||||||
icon: <Plus />,
|
icon: <Plus />,
|
||||||
action: () => navigate('/settings/mcp')
|
action: () => navigate('/mcp-servers')
|
||||||
})
|
})
|
||||||
|
|
||||||
newList.unshift({
|
newList.unshift({
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const SendMessageButton: FC<Props> = ({ disabled, sendMessage }) => {
|
|||||||
style={{
|
style={{
|
||||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||||
color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)',
|
color: disabled ? 'var(--color-text-3)' : 'var(--color-primary)',
|
||||||
fontSize: 22,
|
fontSize: 30,
|
||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
marginRight: 2
|
marginRight: 2
|
||||||
}}
|
}}
|
||||||
|
|||||||
32
src/renderer/src/pages/home/Inputbar/SettingButton.tsx
Normal file
32
src/renderer/src/pages/home/Inputbar/SettingButton.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Assistant } from '@renderer/types'
|
||||||
|
import { Popover } from 'antd'
|
||||||
|
import { SlidersHorizontal } from 'lucide-react'
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
import SettingsTab from '../Tabs/SettingsTab'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
assistant: Assistant
|
||||||
|
ToolbarButton: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingButton: FC<Props> = ({ ToolbarButton }) => {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
arrow={false}
|
||||||
|
placement="topLeft"
|
||||||
|
content={<SettingsTab />}
|
||||||
|
trigger="click"
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: '4px 2px 4px 2px'
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<ToolbarButton type="text">
|
||||||
|
<SlidersHorizontal size={16} />
|
||||||
|
</ToolbarButton>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingButton
|
||||||
@@ -107,7 +107,7 @@ const ThinkingButton: FC<Props> = ({ ref, model, assistant, ToolbarButton }): Re
|
|||||||
}, [currentReasoningEffort, supportedOptions, updateAssistantSettings, model.id])
|
}, [currentReasoningEffort, supportedOptions, updateAssistantSettings, model.id])
|
||||||
|
|
||||||
const createThinkingIcon = useCallback((option?: ThinkingOption, isActive: boolean = false) => {
|
const createThinkingIcon = useCallback((option?: ThinkingOption, isActive: boolean = false) => {
|
||||||
const iconColor = isActive ? 'var(--color-link)' : 'var(--color-icon)'
|
const iconColor = isActive ? 'var(--color-primary)' : 'var(--color-icon)'
|
||||||
|
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case option === 'low':
|
case option === 'low':
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ const WebSearchButton: FC<Props> = ({ ref, assistant, ToolbarButton }) => {
|
|||||||
<Globe
|
<Globe
|
||||||
size={18}
|
size={18}
|
||||||
style={{
|
style={{
|
||||||
color: enableWebSearch ? 'var(--color-link)' : 'var(--color-icon)'
|
color: enableWebSearch ? 'var(--color-primary)' : 'var(--color-icon)'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</ToolbarButton>
|
</ToolbarButton>
|
||||||
|
|||||||
64
src/renderer/src/pages/home/MainSidebar/MainNavbar.tsx
Normal file
64
src/renderer/src/pages/home/MainSidebar/MainNavbar.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { PanelLeftIcon } from '@renderer/components/Icons/PanelIcons'
|
||||||
|
import { isMac } from '@renderer/config/constant'
|
||||||
|
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||||
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
|
import { Tooltip } from 'antd'
|
||||||
|
import { t } from 'i18next'
|
||||||
|
import { MessageSquareDiff } from 'lucide-react'
|
||||||
|
import { FC } from 'react'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface Props {}
|
||||||
|
|
||||||
|
const HeaderNavbar: FC<Props> = () => {
|
||||||
|
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
{showAssistants && (
|
||||||
|
<NavbarIcon onClick={() => toggleShowAssistants()}>
|
||||||
|
<PanelLeftIcon size={18} expanded={true} />
|
||||||
|
</NavbarIcon>
|
||||||
|
)}
|
||||||
|
<Tooltip title={t('settings.shortcuts.new_topic')} mouseEnterDelay={0.8}>
|
||||||
|
<NavbarIcon onClick={() => EventEmitter.emit(EVENT_NAMES.ADD_NEW_TOPIC)}>
|
||||||
|
<MessageSquareDiff size={18} />
|
||||||
|
</NavbarIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
width: var(--assistant-width);
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0;
|
||||||
|
height: var(--navbar-height);
|
||||||
|
min-height: var(--navbar-height);
|
||||||
|
background-color: transparent;
|
||||||
|
-webkit-app-region: drag;
|
||||||
|
padding: 0 15px;
|
||||||
|
padding-left: ${isMac ? '75px' : '15px'};
|
||||||
|
`
|
||||||
|
|
||||||
|
export const NavbarIcon = styled.div`
|
||||||
|
-webkit-app-region: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0 7px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-list-item);
|
||||||
|
color: var(--color-icon-white);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default HeaderNavbar
|
||||||
405
src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx
Normal file
405
src/renderer/src/pages/home/MainSidebar/MainSidebar.tsx
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
|
||||||
|
import UserPopup from '@renderer/components/Popups/UserPopup'
|
||||||
|
import { AppLogo, UserAvatar } from '@renderer/config/env'
|
||||||
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
|
import useAvatar from '@renderer/hooks/useAvatar'
|
||||||
|
import { useChat } from '@renderer/hooks/useChat'
|
||||||
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { useShortcut } from '@renderer/hooks/useShortcuts'
|
||||||
|
import { useShowAssistants } from '@renderer/hooks/useStore'
|
||||||
|
import i18n from '@renderer/i18n'
|
||||||
|
import { getAssistantById } from '@renderer/services/AssistantService'
|
||||||
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
|
import { Assistant, ThemeMode } from '@renderer/types'
|
||||||
|
import { isEmoji } from '@renderer/utils'
|
||||||
|
import { Avatar, Dropdown } from 'antd'
|
||||||
|
import {
|
||||||
|
Blocks,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronRight,
|
||||||
|
CircleHelp,
|
||||||
|
EllipsisVertical,
|
||||||
|
FileSearch,
|
||||||
|
Folder,
|
||||||
|
Languages,
|
||||||
|
LayoutGrid,
|
||||||
|
Moon,
|
||||||
|
Palette,
|
||||||
|
Settings,
|
||||||
|
Sparkle,
|
||||||
|
SquareTerminal,
|
||||||
|
Sun,
|
||||||
|
SunMoon
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { FC, useDeferredValue, useEffect, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import AssistantsTab from '../Tabs/AssistantsTab'
|
||||||
|
import AssistantItem from '../Tabs/components/AssistantItem'
|
||||||
|
import TopicsTab from '../Tabs/TopicsTab'
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
MainMenu,
|
||||||
|
MainMenuItem,
|
||||||
|
MainMenuItemIcon,
|
||||||
|
MainMenuItemLeft,
|
||||||
|
MainMenuItemRight,
|
||||||
|
MainMenuItemText,
|
||||||
|
SubMenu
|
||||||
|
} from './MainSidebarStyles'
|
||||||
|
import OpenedMinappTabs from './OpenedMinapps'
|
||||||
|
import SidebarSearch from './SidebarSearch'
|
||||||
|
|
||||||
|
type Tab = 'assistants' | 'topic'
|
||||||
|
|
||||||
|
const MainSidebar: FC = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [tab, setTab] = useState<Tab>('assistants')
|
||||||
|
const avatar = useAvatar()
|
||||||
|
const { userName, defaultPaintingProvider, transparentWindow } = useSettings()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { theme, settedTheme, toggleTheme } = useTheme()
|
||||||
|
const [isAppMenuExpanded, setIsAppMenuExpanded] = useState(false)
|
||||||
|
const { showAssistants, toggleShowAssistants } = useShowAssistants()
|
||||||
|
|
||||||
|
const location = useLocation()
|
||||||
|
const { pathname } = location
|
||||||
|
|
||||||
|
const { activeAssistant, activeTopic, setActiveAssistant } = useChat()
|
||||||
|
const { showTopics, clickAssistantToShowTopic } = useSettings()
|
||||||
|
|
||||||
|
const { openMinapp } = useMinappPopup()
|
||||||
|
|
||||||
|
const [_searchValue, setSearchValue] = useState('')
|
||||||
|
const searchValue = useDeferredValue(_searchValue)
|
||||||
|
|
||||||
|
useShortcut('toggle_show_assistants', toggleShowAssistants)
|
||||||
|
useShortcut('toggle_show_topics', () => EventEmitter.emit(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR))
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = [
|
||||||
|
EventEmitter.on(EVENT_NAMES.SHOW_TOPIC_SIDEBAR, (assistant: Assistant) => {
|
||||||
|
if (clickAssistantToShowTopic) {
|
||||||
|
setTab('topic')
|
||||||
|
} else {
|
||||||
|
if (activeAssistant.id === assistant.id) {
|
||||||
|
setTab('topic')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => {
|
||||||
|
setTab(tab === 'topic' ? 'assistants' : 'topic')
|
||||||
|
!showAssistants && toggleShowAssistants()
|
||||||
|
})
|
||||||
|
]
|
||||||
|
return () => unsubscribe.forEach((unsubscribe) => unsubscribe())
|
||||||
|
}, [
|
||||||
|
activeAssistant?.id,
|
||||||
|
activeTopic?.assistantId,
|
||||||
|
clickAssistantToShowTopic,
|
||||||
|
isAppMenuExpanded,
|
||||||
|
showAssistants,
|
||||||
|
tab,
|
||||||
|
toggleShowAssistants
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribes = [
|
||||||
|
EventEmitter.on(EVENT_NAMES.SWITCH_ASSISTANT, (assistantId: string) => {
|
||||||
|
const newAssistant = getAssistantById(assistantId)
|
||||||
|
if (newAssistant) {
|
||||||
|
setActiveAssistant(newAssistant)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
EventEmitter.on(EVENT_NAMES.SWITCH_TOPIC_SIDEBAR, () => setTab(tab === 'topic' ? 'assistants' : 'topic')),
|
||||||
|
EventEmitter.on(EVENT_NAMES.OPEN_MINAPP, () => {
|
||||||
|
setTimeout(() => setIsAppMenuExpanded(false), 1000)
|
||||||
|
})
|
||||||
|
]
|
||||||
|
|
||||||
|
return () => unsubscribes.forEach((unsubscribe) => unsubscribe())
|
||||||
|
}, [setActiveAssistant, tab])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canMinimize = !showAssistants && !showTopics
|
||||||
|
window.api.window.setMinimumSize(canMinimize ? 520 : 1080, 600)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.api.window.resetMinimumSize()
|
||||||
|
}
|
||||||
|
}, [showAssistants, showTopics])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsAppMenuExpanded(false)
|
||||||
|
}, [activeAssistant.id, activeTopic.id])
|
||||||
|
|
||||||
|
const appMenuItems = [
|
||||||
|
{ icon: <Sparkle size={18} className="icon" />, text: t('agents.title'), path: '/agents' },
|
||||||
|
{ icon: <Languages size={18} className="icon" />, text: t('translate.title'), path: '/translate' },
|
||||||
|
{
|
||||||
|
icon: <Palette size={18} className="icon" />,
|
||||||
|
text: t('paintings.title'),
|
||||||
|
path: `/paintings/${defaultPaintingProvider}`
|
||||||
|
},
|
||||||
|
{ icon: <LayoutGrid size={18} className="icon" />, text: t('minapp.title'), path: '/apps' },
|
||||||
|
{ icon: <FileSearch size={18} className="icon" />, text: t('knowledge.title'), path: '/knowledge' },
|
||||||
|
{ icon: <SquareTerminal size={18} className="icon" />, text: t('settings.mcp.title'), path: '/mcp-servers' },
|
||||||
|
{ icon: <Folder size={18} className="icon" />, text: t('files.title'), path: '/files' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const isRoutes = (path: string): boolean => pathname.startsWith(path)
|
||||||
|
|
||||||
|
const docsId = 'cherrystudio-docs'
|
||||||
|
const onOpenDocs = () => {
|
||||||
|
const isChinese = i18n.language.startsWith('zh')
|
||||||
|
openMinapp({
|
||||||
|
id: docsId,
|
||||||
|
name: t('docs.title'),
|
||||||
|
url: isChinese ? 'https://docs.cherry-ai.com/' : 'https://docs.cherry-ai.com/cherry-studio-wen-dang/en-us',
|
||||||
|
logo: AppLogo
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!showAssistants) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container
|
||||||
|
id="main-sidebar"
|
||||||
|
transparent={transparentWindow}
|
||||||
|
style={{
|
||||||
|
width: showAssistants ? 'var(--assistants-width)' : '0px',
|
||||||
|
opacity: showAssistants ? 1 : 0,
|
||||||
|
overflow: showAssistants ? 'initial' : 'hidden'
|
||||||
|
}}>
|
||||||
|
<MainMenu>
|
||||||
|
<SidebarSearch onSearch={setSearchValue} />
|
||||||
|
<MainMenuItem active={isAppMenuExpanded} onClick={() => setIsAppMenuExpanded(!isAppMenuExpanded)}>
|
||||||
|
<MainMenuItemLeft>
|
||||||
|
<MainMenuItemIcon>
|
||||||
|
<Blocks size={19} className="icon" />
|
||||||
|
</MainMenuItemIcon>
|
||||||
|
<MainMenuItemText>{isAppMenuExpanded ? t('common.collapse') : t('common.apps')}</MainMenuItemText>
|
||||||
|
</MainMenuItemLeft>
|
||||||
|
<MainMenuItemRight>
|
||||||
|
{isAppMenuExpanded ? (
|
||||||
|
<ChevronDown size={18} color="var(--color-text-3)" />
|
||||||
|
) : (
|
||||||
|
<ChevronRight size={18} color="var(--color-text-3)" />
|
||||||
|
)}
|
||||||
|
</MainMenuItemRight>
|
||||||
|
</MainMenuItem>
|
||||||
|
{isAppMenuExpanded && (
|
||||||
|
<SubMenu>
|
||||||
|
{appMenuItems.map((item) => (
|
||||||
|
<MainMenuItem key={item.path} active={isRoutes(item.path)} onClick={() => navigate(item.path)}>
|
||||||
|
<MainMenuItemLeft>
|
||||||
|
<MainMenuItemIcon>{item.icon}</MainMenuItemIcon>
|
||||||
|
<MainMenuItemText>{item.text}</MainMenuItemText>
|
||||||
|
</MainMenuItemLeft>
|
||||||
|
</MainMenuItem>
|
||||||
|
))}
|
||||||
|
</SubMenu>
|
||||||
|
)}
|
||||||
|
<OpenedMinappTabs />
|
||||||
|
</MainMenu>
|
||||||
|
{tab === 'topic' && (
|
||||||
|
<AssistantContainer onClick={() => setIsAppMenuExpanded(false)}>
|
||||||
|
<AssistantItem
|
||||||
|
key={activeAssistant.id}
|
||||||
|
assistant={activeAssistant}
|
||||||
|
isActive={false}
|
||||||
|
sortBy="list"
|
||||||
|
onSwitch={() => {}}
|
||||||
|
onDelete={() => {}}
|
||||||
|
addAssistant={() => {}}
|
||||||
|
onCreateDefaultAssistant={() => {}}
|
||||||
|
handleSortByChange={() => {}}
|
||||||
|
singleLine
|
||||||
|
/>
|
||||||
|
</AssistantContainer>
|
||||||
|
)}
|
||||||
|
<MainContainer>
|
||||||
|
{tab === 'assistants' && <AssistantsTab searchValue={searchValue} />}
|
||||||
|
{tab === 'topic' && <TopicsTab searchValue={searchValue} style={{ paddingTop: 4 }} />}
|
||||||
|
</MainContainer>
|
||||||
|
<UserMenu>
|
||||||
|
<UserMenuLeft onClick={() => UserPopup.show()}>
|
||||||
|
{isEmoji(avatar) ? (
|
||||||
|
<EmojiAvatar className="sidebar-avatar" size={31} fontSize={18}>
|
||||||
|
{avatar}
|
||||||
|
</EmojiAvatar>
|
||||||
|
) : (
|
||||||
|
<AvatarImg src={avatar || UserAvatar} draggable={false} className="nodrag" />
|
||||||
|
)}
|
||||||
|
{userName && <UserMenuText>{userName}</UserMenuText>}
|
||||||
|
</UserMenuLeft>
|
||||||
|
<Dropdown
|
||||||
|
placement="topRight"
|
||||||
|
trigger={['click']}
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: 'theme',
|
||||||
|
label: (
|
||||||
|
<span
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
toggleTheme()
|
||||||
|
}}>
|
||||||
|
{t('settings.theme.title')}: {t(`settings.theme.${settedTheme}`)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
icon: ThemeIcon()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'about',
|
||||||
|
label: t('docs.title'),
|
||||||
|
icon: <CircleHelp size={16} className="icon" />,
|
||||||
|
onClick: onOpenDocs
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'settings',
|
||||||
|
label: t('settings.title'),
|
||||||
|
icon: <Settings size={16} className="icon" />,
|
||||||
|
onClick: () => window.api.showSettingsWindow({ defaultTab: 'provider' })
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}}>
|
||||||
|
<Icon theme={theme} className="settings-icon">
|
||||||
|
<EllipsisVertical size={16} />
|
||||||
|
</Icon>
|
||||||
|
</Dropdown>
|
||||||
|
</UserMenu>
|
||||||
|
</Container>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeIcon = () => {
|
||||||
|
const { settedTheme } = useTheme()
|
||||||
|
|
||||||
|
return settedTheme === ThemeMode.dark ? (
|
||||||
|
<Moon size={16} className="icon" />
|
||||||
|
) : settedTheme === ThemeMode.light ? (
|
||||||
|
<Sun size={16} className="icon" />
|
||||||
|
) : (
|
||||||
|
<SunMoon size={16} className="icon" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const MainContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const AssistantContainer = styled.div`
|
||||||
|
margin: 4px 10px;
|
||||||
|
display: flex;
|
||||||
|
margin-top: 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
const UserMenu = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
gap: 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const UserMenuLeft = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-list-item);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const AvatarImg = styled(Avatar)`
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background-color: var(--color-background-soft);
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
`
|
||||||
|
|
||||||
|
const UserMenuText = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-right: 3px;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Icon = styled.div<{ theme: string }>`
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
-webkit-app-region: none;
|
||||||
|
border: 0.5px solid transparent;
|
||||||
|
&.settings-icon {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-list-item);
|
||||||
|
opacity: 0.8;
|
||||||
|
cursor: pointer;
|
||||||
|
.icon {
|
||||||
|
color: var(--color-icon-white);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-list-item);
|
||||||
|
border: 0.5px solid var(--color-border);
|
||||||
|
.icon {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes borderBreath {
|
||||||
|
0% {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.opened-minapp {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
&.opened-minapp::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
opacity: 0.3;
|
||||||
|
border: 0.5px solid var(--color-primary);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default MainSidebar
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
export const MainMenuItem = styled.div<{ active?: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'transparent')};
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
opacity: ${({ active }) => (active ? 0.6 : 1)};
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-list-item);
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
background-color: ${({ active }) => (active ? 'var(--color-list-item)' : 'var(--color-list-item-hover)')};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const MainMenuItemLeft = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const MainMenuItemRight = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-right: -3px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const MainMenuItemIcon = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const MainMenuItemText = styled.div`
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Container = styled.div<{ transparent?: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
width: var(--assistants-width);
|
||||||
|
max-width: var(--assistants-width);
|
||||||
|
border-right: 0.5px solid var(--color-border);
|
||||||
|
height: calc(var(--main-height) - 50px);
|
||||||
|
min-height: calc(var(--main-height) - 50px);
|
||||||
|
background: var(--color-background);
|
||||||
|
padding-top: 10px;
|
||||||
|
margin-top: 50px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const MainMenu = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 10px;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const SubMenu = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 5px 0;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const TabsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
-webkit-app-region: none;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 5px;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export const TabsWrapper = styled(Scrollbar as any)`
|
||||||
|
width: 100%;
|
||||||
|
max-height: 50vh;
|
||||||
|
`
|
||||||
|
|
||||||
|
export const Menus = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
`
|
||||||
167
src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx
Normal file
167
src/renderer/src/pages/home/MainSidebar/OpenedMinapps.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
import { DraggableList } from '@renderer/components/DraggableList'
|
||||||
|
import MinAppIcon from '@renderer/components/Icons/MinAppIcon'
|
||||||
|
import IndicatorLight from '@renderer/components/IndicatorLight'
|
||||||
|
import { Center } from '@renderer/components/Layout'
|
||||||
|
import { useMinappPopup } from '@renderer/hooks/useMinappPopup'
|
||||||
|
import { useMinapps } from '@renderer/hooks/useMinapps'
|
||||||
|
import { useRuntime } from '@renderer/hooks/useRuntime'
|
||||||
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import type { MenuProps } from 'antd'
|
||||||
|
import { Empty } from 'antd'
|
||||||
|
import { Dropdown } from 'antd'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
import { FC, useEffect, useMemo } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import {
|
||||||
|
MainMenuItem,
|
||||||
|
MainMenuItemIcon,
|
||||||
|
MainMenuItemLeft,
|
||||||
|
MainMenuItemRight,
|
||||||
|
MainMenuItemText,
|
||||||
|
TabsContainer,
|
||||||
|
TabsWrapper
|
||||||
|
} from './MainSidebarStyles'
|
||||||
|
|
||||||
|
const OpenedMinapps: FC = () => {
|
||||||
|
const { minappShow, openedKeepAliveMinapps, currentMinappId } = useRuntime()
|
||||||
|
const { openMinappKeepAlive, hideMinappPopup, closeMinapp, closeAllMinapps } = useMinappPopup()
|
||||||
|
const { showOpenedMinappsInSidebar } = useSettings()
|
||||||
|
const { pinned, updatePinnedMinapps } = useMinapps()
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
// 合并并排序应用列表
|
||||||
|
const sortedApps = useMemo(() => {
|
||||||
|
// 分离已打开但未固定的应用
|
||||||
|
const openedNotPinned = openedKeepAliveMinapps.filter((app) => !pinned.find((p) => p.id === app.id))
|
||||||
|
|
||||||
|
// 获取固定应用列表(保持原有顺序)
|
||||||
|
const pinnedApps = pinned.map((app) => {
|
||||||
|
const openedApp = openedKeepAliveMinapps.find((o) => o.id === app.id)
|
||||||
|
return openedApp || app
|
||||||
|
})
|
||||||
|
|
||||||
|
// 把已启动但未固定的放到列表下面
|
||||||
|
return [...pinnedApps, ...openedNotPinned]
|
||||||
|
}, [openedKeepAliveMinapps, pinned])
|
||||||
|
|
||||||
|
const handleOnClick = (app) => {
|
||||||
|
if (minappShow && currentMinappId === app.id) {
|
||||||
|
hideMinappPopup()
|
||||||
|
} else {
|
||||||
|
openMinappKeepAlive(app)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const iconDefaultHeight = 40
|
||||||
|
const iconDefaultOffset = 17
|
||||||
|
const container = document.querySelector('.TabsContainer') as HTMLElement
|
||||||
|
const activeIcon = document.querySelector('.TabsContainer .opened-active') as HTMLElement
|
||||||
|
|
||||||
|
let indicatorTop = 0,
|
||||||
|
indicatorRight = 0
|
||||||
|
if (minappShow && activeIcon && container) {
|
||||||
|
indicatorTop = activeIcon.offsetTop + activeIcon.offsetHeight / 2 - 4
|
||||||
|
indicatorRight = 0
|
||||||
|
} else {
|
||||||
|
indicatorTop =
|
||||||
|
((openedKeepAliveMinapps.length > 0 ? openedKeepAliveMinapps.length : 1) / 2) * iconDefaultHeight +
|
||||||
|
iconDefaultOffset -
|
||||||
|
4
|
||||||
|
indicatorRight = -50
|
||||||
|
}
|
||||||
|
container.style.setProperty('--indicator-top', `${indicatorTop}px`)
|
||||||
|
container.style.setProperty('--indicator-right', `${indicatorRight}px`)
|
||||||
|
}, [currentMinappId, openedKeepAliveMinapps, minappShow])
|
||||||
|
|
||||||
|
const isShowApps = showOpenedMinappsInSidebar && sortedApps.length > 0
|
||||||
|
|
||||||
|
if (!isShowApps) return <TabsContainer className="TabsContainer" />
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TabsContainer className="TabsContainer" style={{ marginBottom: 4 }}>
|
||||||
|
<Divider />
|
||||||
|
<TabsWrapper>
|
||||||
|
<DraggableList
|
||||||
|
list={sortedApps}
|
||||||
|
onUpdate={(newList) => {
|
||||||
|
// 只更新固定应用的顺序
|
||||||
|
const newPinned = newList.filter((app) => pinned.find((p) => p.id === app.id))
|
||||||
|
updatePinnedMinapps(newPinned)
|
||||||
|
}}
|
||||||
|
listStyle={{ margin: '4px 0' }}>
|
||||||
|
{(app) => {
|
||||||
|
const isPinned = pinned.find((p) => p.id === app.id)
|
||||||
|
const isOpened = openedKeepAliveMinapps.find((o) => o.id === app.id)
|
||||||
|
|
||||||
|
const menuItems: MenuProps['items'] = [
|
||||||
|
{
|
||||||
|
key: 'togglePin',
|
||||||
|
label: isPinned ? t('minapp.sidebar.remove.title') : t('minapp.sidebar.pin.title'),
|
||||||
|
onClick: () => {
|
||||||
|
if (isPinned) {
|
||||||
|
const newPinned = pinned.filter((item) => item.id !== app.id)
|
||||||
|
updatePinnedMinapps(newPinned)
|
||||||
|
} else {
|
||||||
|
updatePinnedMinapps([...pinned, app])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (isOpened) {
|
||||||
|
menuItems.push(
|
||||||
|
{
|
||||||
|
key: 'closeApp',
|
||||||
|
label: t('minapp.sidebar.close.title'),
|
||||||
|
onClick: () => closeMinapp(app.id)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'closeAllApp',
|
||||||
|
label: t('minapp.sidebar.closeall.title'),
|
||||||
|
onClick: () => closeAllMinapps()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} overlayStyle={{ zIndex: 10000 }}>
|
||||||
|
<MainMenuItem key={app.id} onClick={() => handleOnClick(app)}>
|
||||||
|
<MainMenuItemLeft>
|
||||||
|
<MainMenuItemIcon>
|
||||||
|
<MinAppIcon size={22} app={app} style={{ borderRadius: 6 }} sidebar />
|
||||||
|
</MainMenuItemIcon>
|
||||||
|
<MainMenuItemText>{app.name}</MainMenuItemText>
|
||||||
|
</MainMenuItemLeft>
|
||||||
|
{isOpened && (
|
||||||
|
<MainMenuItemRight style={{ marginRight: 4 }}>
|
||||||
|
<IndicatorLight color="var(--color-primary)" shadow={false} animation={false} size={5} />
|
||||||
|
</MainMenuItemRight>
|
||||||
|
)}
|
||||||
|
</MainMenuItem>
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</DraggableList>
|
||||||
|
{isEmpty(sortedApps) && (
|
||||||
|
<Center>
|
||||||
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||||
|
</Center>
|
||||||
|
)}
|
||||||
|
</TabsWrapper>
|
||||||
|
<Divider />
|
||||||
|
</TabsContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Divider = styled.div`
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--color-border);
|
||||||
|
margin: 5px 0;
|
||||||
|
opacity: 0.5;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default OpenedMinapps
|
||||||
106
src/renderer/src/pages/home/MainSidebar/SidebarSearch.tsx
Normal file
106
src/renderer/src/pages/home/MainSidebar/SidebarSearch.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { Input, InputRef } from 'antd'
|
||||||
|
import { Search } from 'lucide-react'
|
||||||
|
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import { MainMenuItem, MainMenuItemIcon, MainMenuItemLeft, MainMenuItemText } from './MainSidebarStyles'
|
||||||
|
|
||||||
|
interface SidebarSearchProps {
|
||||||
|
onSearch: (text: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarSearch: React.FC<SidebarSearchProps> = ({ onSearch }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [isExpanded, setIsExpanded] = useState(false)
|
||||||
|
const [searchText, setSearchText] = useState('')
|
||||||
|
const inputRef = useRef<InputRef>(null)
|
||||||
|
|
||||||
|
const handleTextChange = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
setSearchText(text)
|
||||||
|
onSearch(text)
|
||||||
|
},
|
||||||
|
[onSearch]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleExpand = useCallback(() => {
|
||||||
|
setIsExpanded(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleClear = useCallback(() => {
|
||||||
|
setSearchText('')
|
||||||
|
onSearch('')
|
||||||
|
}, [onSearch])
|
||||||
|
|
||||||
|
const handleCollapse = useCallback(() => {
|
||||||
|
setSearchText('')
|
||||||
|
setIsExpanded(false)
|
||||||
|
onSearch('')
|
||||||
|
}, [onSearch])
|
||||||
|
|
||||||
|
const handleInputKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
handleCollapse()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleCollapse]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isExpanded && inputRef.current) {
|
||||||
|
inputRef.current.focus()
|
||||||
|
}
|
||||||
|
}, [isExpanded])
|
||||||
|
|
||||||
|
const renderInputBox = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={searchText}
|
||||||
|
placeholder={t('chat.assistant.search.placeholder')}
|
||||||
|
onChange={(e) => handleTextChange(e.target.value)}
|
||||||
|
onKeyDown={handleInputKeyDown}
|
||||||
|
onBlur={(e) => {
|
||||||
|
// 如果输入框失焦且没有搜索内容,则收起
|
||||||
|
if (!e.target.value.trim()) {
|
||||||
|
handleCollapse()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClear={handleClear}
|
||||||
|
allowClear
|
||||||
|
style={{
|
||||||
|
paddingTop: 4
|
||||||
|
}}
|
||||||
|
prefix={
|
||||||
|
<MainMenuItemIcon style={{ margin: '0 6px 0 -2px' }}>
|
||||||
|
<Search size={18} className="icon" />
|
||||||
|
</MainMenuItemIcon>
|
||||||
|
}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}, [handleClear, handleCollapse, handleInputKeyDown, handleTextChange, searchText, t])
|
||||||
|
|
||||||
|
const renderMenuItem = useMemo(() => {
|
||||||
|
return (
|
||||||
|
<MainMenuItem onClick={handleExpand} style={{ cursor: 'pointer' }}>
|
||||||
|
<MainMenuItemLeft>
|
||||||
|
<MainMenuItemIcon>
|
||||||
|
<Search size={18} className="icon" />
|
||||||
|
</MainMenuItemIcon>
|
||||||
|
<MainMenuItemText>{t('chat.assistant.search.placeholder')}</MainMenuItemText>
|
||||||
|
</MainMenuItemLeft>
|
||||||
|
</MainMenuItem>
|
||||||
|
)
|
||||||
|
}, [handleExpand, t])
|
||||||
|
|
||||||
|
return <SearchBarWrapper>{isExpanded ? renderInputBox : renderMenuItem}</SearchBarWrapper>
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchBarWrapper = styled.div`
|
||||||
|
height: 2.2rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
export default memo(SidebarSearch)
|
||||||
@@ -96,15 +96,19 @@ const Markdown: FC<Props> = ({ block }) => {
|
|||||||
} as Partial<Components>
|
} as Partial<Components>
|
||||||
}, [onSaveCodeBlock, block.id])
|
}, [onSaveCodeBlock, block.id])
|
||||||
|
|
||||||
if (messageContent.includes('<style>')) {
|
|
||||||
components.style = MarkdownShadowDOMRenderer as any
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlTransform = useCallback((value: string) => {
|
const urlTransform = useCallback((value: string) => {
|
||||||
if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value
|
if (value.startsWith('data:image/png') || value.startsWith('data:image/jpeg')) return value
|
||||||
return defaultUrlTransform(value)
|
return defaultUrlTransform(value)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// if (role === 'user' && !renderInputMessageAsMarkdown) {
|
||||||
|
// return <p style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>{messageContent}</p>
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (messageContent.includes('<style>')) {
|
||||||
|
components.style = MarkdownShadowDOMRenderer as any
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="markdown">
|
<div className="markdown">
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const MainTextBlock: React.FC<Props> = ({ block, citationBlockId, role, mentions
|
|||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
{role === 'user' && !renderInputMessageAsMarkdown ? (
|
{role === 'user' && !renderInputMessageAsMarkdown ? (
|
||||||
<p className="markdown" style={{ whiteSpace: 'pre-wrap' }}>
|
<p className="markdown" style={{ marginBottom: 5, whiteSpace: 'pre-wrap' }}>
|
||||||
{block.content}
|
{block.content}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
VerticalAlignBottomOutlined,
|
VerticalAlignBottomOutlined,
|
||||||
VerticalAlignTopOutlined
|
VerticalAlignTopOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
|
||||||
import { RootState } from '@renderer/store'
|
import { RootState } from '@renderer/store'
|
||||||
// import { selectCurrentTopicId } from '@renderer/store/newMessage'
|
// import { selectCurrentTopicId } from '@renderer/store/newMessage'
|
||||||
import { Button, Drawer, Tooltip } from 'antd'
|
import { Button, Drawer, Tooltip } from 'antd'
|
||||||
@@ -44,8 +43,6 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
|||||||
const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null)
|
const [manuallyClosedUntil, setManuallyClosedUntil] = useState<number | null>(null)
|
||||||
const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId)
|
const currentTopicId = useSelector((state: RootState) => state.messages.currentTopicId)
|
||||||
const lastMoveTime = useRef(0)
|
const lastMoveTime = useRef(0)
|
||||||
const { topicPosition, showTopics } = useSettings()
|
|
||||||
const showRightTopics = topicPosition === 'right' && showTopics
|
|
||||||
|
|
||||||
// Reset hide timer and make buttons visible
|
// Reset hide timer and make buttons visible
|
||||||
const resetHideTimer = useCallback(() => {
|
const resetHideTimer = useCallback(() => {
|
||||||
@@ -274,14 +271,7 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
|||||||
// Calculate if the mouse is in the trigger area
|
// Calculate if the mouse is in the trigger area
|
||||||
const triggerWidth = 60 // Same as the width in styled component
|
const triggerWidth = 60 // Same as the width in styled component
|
||||||
|
|
||||||
// Safe way to calculate position when using calc expressions
|
const rightPosition = window.innerWidth - triggerWidth
|
||||||
let rightOffset = RIGHT_GAP // Default right offset
|
|
||||||
if (showRightTopics) {
|
|
||||||
// When topics are shown on right, we need to account for topic list width
|
|
||||||
rightOffset += 275 // --topic-list-width
|
|
||||||
}
|
|
||||||
|
|
||||||
const rightPosition = window.innerWidth - rightOffset - triggerWidth
|
|
||||||
const topPosition = window.innerHeight * 0.35 // 35% from top
|
const topPosition = window.innerHeight * 0.35 // 35% from top
|
||||||
const height = window.innerHeight * 0.3 // 30% of window height
|
const height = window.innerHeight * 0.3 // 30% of window height
|
||||||
|
|
||||||
@@ -326,16 +316,7 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
|||||||
clearTimeout(hideTimer)
|
clearTimeout(hideTimer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [
|
}, [containerId, hideTimer, resetHideTimer, isNearButtons, handleMouseEnter, handleMouseLeave, manuallyClosedUntil])
|
||||||
containerId,
|
|
||||||
hideTimer,
|
|
||||||
resetHideTimer,
|
|
||||||
isNearButtons,
|
|
||||||
handleMouseEnter,
|
|
||||||
handleMouseLeave,
|
|
||||||
showRightTopics,
|
|
||||||
manuallyClosedUntil
|
|
||||||
])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -151,9 +151,7 @@ const WebSearchCitation: React.FC<{ citation: Citation }> = ({ citation }) => {
|
|||||||
<CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
|
<CitationLink className="text-nowrap" href={citation.url} onClick={(e) => handleLinkClick(citation.url, e)}>
|
||||||
{citation.title || <span className="hostname">{citation.hostname}</span>}
|
{citation.title || <span className="hostname">{citation.hostname}</span>}
|
||||||
</CitationLink>
|
</CitationLink>
|
||||||
|
<CitationIndex>{citation.number}</CitationIndex>s{fetchedContent && <CopyButton content={fetchedContent} />}
|
||||||
<CitationIndex>{citation.number}</CitationIndex>
|
|
||||||
{fetchedContent && <CopyButton content={fetchedContent} />}
|
|
||||||
</WebSearchCardHeader>
|
</WebSearchCardHeader>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Skeleton active paragraph={{ rows: 1 }} title={false} />
|
<Skeleton active paragraph={{ rows: 1 }} title={false} />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMessageEditing } from '@renderer/context/MessageEditingContext'
|
|||||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||||
import { useModel } from '@renderer/hooks/useModel'
|
import { useModel } from '@renderer/hooks/useModel'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||||
import { getModelUniqId } from '@renderer/services/ModelService'
|
import { getModelUniqId } from '@renderer/services/ModelService'
|
||||||
@@ -21,6 +21,7 @@ import MessageEditor from './MessageEditor'
|
|||||||
import MessageErrorBoundary from './MessageErrorBoundary'
|
import MessageErrorBoundary from './MessageErrorBoundary'
|
||||||
import MessageHeader from './MessageHeader'
|
import MessageHeader from './MessageHeader'
|
||||||
import MessageMenubar from './MessageMenubar'
|
import MessageMenubar from './MessageMenubar'
|
||||||
|
import MessageTokens from './MessageTokens'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message
|
message: Message
|
||||||
@@ -42,12 +43,14 @@ const MessageItem: FC<Props> = ({
|
|||||||
index,
|
index,
|
||||||
hideMenuBar = false,
|
hideMenuBar = false,
|
||||||
isGrouped,
|
isGrouped,
|
||||||
isStreaming = false
|
isStreaming = false,
|
||||||
|
style
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||||
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
const model = useModel(getMessageModelId(message), message.model?.provider) || message.model
|
||||||
const { messageFont, fontSize, messageStyle } = useSettings()
|
const { isBubbleStyle } = useMessageStyle()
|
||||||
|
const { showMessageDivider, messageFont, fontSize, messageStyle } = useSettings()
|
||||||
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
|
const { editMessageBlocks, resendUserMessageWithEdit, editMessage } = useMessageOperations(topic)
|
||||||
const messageContainerRef = useRef<HTMLDivElement>(null)
|
const messageContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const { editingMessageId, stopEditing } = useMessageEditing()
|
const { editingMessageId, stopEditing } = useMessageEditing()
|
||||||
@@ -99,6 +102,8 @@ const MessageItem: FC<Props> = ({
|
|||||||
const isAssistantMessage = message.role === 'assistant'
|
const isAssistantMessage = message.role === 'assistant'
|
||||||
const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing
|
const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing
|
||||||
|
|
||||||
|
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
|
||||||
|
|
||||||
const messageHighlightHandler = useCallback((highlight: boolean = true) => {
|
const messageHighlightHandler = useCallback((highlight: boolean = true) => {
|
||||||
if (messageContainerRef.current) {
|
if (messageContainerRef.current) {
|
||||||
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
|
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||||
@@ -127,6 +132,29 @@ const MessageItem: FC<Props> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<MessageContainer style={{ paddingTop: 15 }}>
|
||||||
|
<MessageHeader
|
||||||
|
message={message}
|
||||||
|
assistant={assistant}
|
||||||
|
model={model}
|
||||||
|
key={getModelUniqId(model)}
|
||||||
|
topic={topic}
|
||||||
|
/>
|
||||||
|
<div style={{ paddingLeft: messageStyle === 'plain' ? 46 : undefined }}>
|
||||||
|
<MessageEditor
|
||||||
|
message={message}
|
||||||
|
topicId={topic.id}
|
||||||
|
onSave={handleEditSave}
|
||||||
|
onResend={handleEditResend}
|
||||||
|
onCancel={handleEditCancel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</MessageContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageContainer
|
<MessageContainer
|
||||||
key={message.id}
|
key={message.id}
|
||||||
@@ -135,58 +163,55 @@ const MessageItem: FC<Props> = ({
|
|||||||
'message-assistant': isAssistantMessage,
|
'message-assistant': isAssistantMessage,
|
||||||
'message-user': !isAssistantMessage
|
'message-user': !isAssistantMessage
|
||||||
})}
|
})}
|
||||||
ref={messageContainerRef}>
|
ref={messageContainerRef}
|
||||||
|
style={style}>
|
||||||
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} topic={topic} />
|
<MessageHeader message={message} assistant={assistant} model={model} key={getModelUniqId(model)} topic={topic} />
|
||||||
{isEditing && (
|
<MessageContentContainer
|
||||||
<MessageEditor
|
className="message-content-container"
|
||||||
message={message}
|
style={{
|
||||||
topicId={topic.id}
|
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
|
||||||
onSave={handleEditSave}
|
fontSize,
|
||||||
onResend={handleEditResend}
|
background: messageBackground,
|
||||||
onCancel={handleEditCancel}
|
overflowY: 'visible'
|
||||||
/>
|
}}>
|
||||||
)}
|
<MessageErrorBoundary>
|
||||||
{!isEditing && (
|
<MessageContent message={message} />
|
||||||
<>
|
</MessageErrorBoundary>
|
||||||
<MessageContentContainer
|
</MessageContentContainer>
|
||||||
className="message-content-container"
|
{showMenubar && (
|
||||||
style={{
|
<MessageFooter className="MessageFooter" $isLastMessage={isLastMessage} $messageStyle={messageStyle}>
|
||||||
fontFamily: messageFont === 'serif' ? 'var(--font-family-serif)' : 'var(--font-family)',
|
<MessageMenubar
|
||||||
fontSize,
|
message={message}
|
||||||
overflowY: 'visible'
|
assistant={assistant}
|
||||||
}}>
|
model={model}
|
||||||
<MessageErrorBoundary>
|
index={index}
|
||||||
<MessageContent message={message} />
|
topic={topic}
|
||||||
</MessageErrorBoundary>
|
isLastMessage={isLastMessage}
|
||||||
</MessageContentContainer>
|
isAssistantMessage={isAssistantMessage}
|
||||||
{showMenubar && (
|
isGrouped={isGrouped}
|
||||||
<MessageFooter className="MessageFooter" $isLastMessage={isLastMessage} $messageStyle={messageStyle}>
|
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
|
||||||
<MessageMenubar
|
setModel={setModel}
|
||||||
message={message}
|
/>
|
||||||
assistant={assistant}
|
</MessageFooter>
|
||||||
model={model}
|
|
||||||
index={index}
|
|
||||||
topic={topic}
|
|
||||||
isLastMessage={isLastMessage}
|
|
||||||
isAssistantMessage={isAssistantMessage}
|
|
||||||
isGrouped={isGrouped}
|
|
||||||
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
|
|
||||||
setModel={setModel}
|
|
||||||
/>
|
|
||||||
</MessageFooter>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</MessageContainer>
|
</MessageContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) => {
|
||||||
|
return isBubbleStyle
|
||||||
|
? isAssistantMessage
|
||||||
|
? 'var(--chat-background-assistant)'
|
||||||
|
: 'var(--chat-background-user)'
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
|
||||||
const MessageContainer = styled.div`
|
const MessageContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
|
padding: 0 24px;
|
||||||
transform: translateZ(0);
|
transform: translateZ(0);
|
||||||
will-change: transform;
|
will-change: transform;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -226,11 +251,11 @@ const MessageFooter = styled.div<{ $isLastMessage: boolean; $messageStyle: 'plai
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-left: 46px;
|
margin-left: 46px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
border-top: 0.5px dotted var(--color-border);
|
||||||
`
|
`
|
||||||
|
|
||||||
const NewContextMessage = styled.div`
|
const NewContextMessage = styled.div`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
flex: 1;
|
|
||||||
`
|
`
|
||||||
|
|
||||||
export default memo(MessageItem)
|
export default memo(MessageItem)
|
||||||
|
|||||||
@@ -1,6 +1,21 @@
|
|||||||
|
import {
|
||||||
|
FileExcelFilled,
|
||||||
|
FileImageFilled,
|
||||||
|
FileMarkdownFilled,
|
||||||
|
FilePdfFilled,
|
||||||
|
FilePptFilled,
|
||||||
|
FileTextFilled,
|
||||||
|
FileUnknownFilled,
|
||||||
|
FileWordFilled,
|
||||||
|
FileZipFilled,
|
||||||
|
FolderOpenFilled,
|
||||||
|
GlobalOutlined,
|
||||||
|
LinkOutlined
|
||||||
|
} from '@ant-design/icons'
|
||||||
|
import CustomTag from '@renderer/components/CustomTag'
|
||||||
import FileManager from '@renderer/services/FileManager'
|
import FileManager from '@renderer/services/FileManager'
|
||||||
|
import { FileType } from '@renderer/types'
|
||||||
import type { FileMessageBlock } from '@renderer/types/newMessage'
|
import type { FileMessageBlock } from '@renderer/types/newMessage'
|
||||||
import { Upload } from 'antd'
|
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
@@ -8,17 +23,6 @@ interface Props {
|
|||||||
block: FileMessageBlock
|
block: FileMessageBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledUpload = styled(Upload)`
|
|
||||||
.ant-upload-list-item-name {
|
|
||||||
max-width: 220px;
|
|
||||||
display: inline-block;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const MessageAttachments: FC<Props> = ({ block }) => {
|
const MessageAttachments: FC<Props> = ({ block }) => {
|
||||||
// const handleCopyImage = async (image: FileMetadata) => {
|
// const handleCopyImage = async (image: FileMetadata) => {
|
||||||
// const data = await FileManager.readFile(image)
|
// const data = await FileManager.readFile(image)
|
||||||
@@ -30,54 +34,84 @@ const MessageAttachments: FC<Props> = ({ block }) => {
|
|||||||
if (!block.file) {
|
if (!block.file) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
// 由图片块代替
|
|
||||||
// if (block.file.type === FileTypes.IMAGE) {
|
const MAX_FILENAME_DISPLAY_LENGTH = 20
|
||||||
// return (
|
function truncateFileName(name: string, maxLength: number = MAX_FILENAME_DISPLAY_LENGTH) {
|
||||||
// <Container style={{ marginBottom: 8 }}>
|
if (name.length <= maxLength) return name
|
||||||
// <Image
|
return name.slice(0, maxLength - 3) + '...'
|
||||||
// src={FileManager.getFileUrl(block.file)}
|
}
|
||||||
// key={block.file.id}
|
|
||||||
// width="33%"
|
const getFileIcon = (type?: string) => {
|
||||||
// preview={{
|
if (!type) return <FileUnknownFilled />
|
||||||
// toolbarRender: (
|
|
||||||
// _,
|
const ext = type.toLowerCase()
|
||||||
// {
|
|
||||||
// transform: { scale },
|
if (['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'].includes(ext)) {
|
||||||
// actions: { onFlipY, onFlipX, onRotateLeft, onRotateRight, onZoomOut, onZoomIn, onReset }
|
return <FileImageFilled />
|
||||||
// }
|
}
|
||||||
// ) => (
|
|
||||||
// <ToobarWrapper size={12} className="toolbar-wrapper">
|
if (['.doc', '.docx'].includes(ext)) {
|
||||||
// <SwapOutlined rotate={90} onClick={onFlipY} />
|
return <FileWordFilled />
|
||||||
// <SwapOutlined onClick={onFlipX} />
|
}
|
||||||
// <RotateLeftOutlined onClick={onRotateLeft} />
|
if (['.xls', '.xlsx'].includes(ext)) {
|
||||||
// <RotateRightOutlined onClick={onRotateRight} />
|
return <FileExcelFilled />
|
||||||
// <ZoomOutOutlined disabled={scale === 1} onClick={onZoomOut} />
|
}
|
||||||
// <ZoomInOutlined disabled={scale === 50} onClick={onZoomIn} />
|
if (['.ppt', '.pptx'].includes(ext)) {
|
||||||
// <UndoOutlined onClick={onReset} />
|
return <FilePptFilled />
|
||||||
// <CopyOutlined onClick={() => handleCopyImage(block.file)} />
|
}
|
||||||
// <DownloadOutlined onClick={() => download(FileManager.getFileUrl(block.file))} />
|
if (ext === '.pdf') {
|
||||||
// </ToobarWrapper>
|
return <FilePdfFilled />
|
||||||
// )
|
}
|
||||||
// }}
|
if (['.md', '.markdown'].includes(ext)) {
|
||||||
// />
|
return <FileMarkdownFilled />
|
||||||
// </Container>
|
}
|
||||||
// )
|
|
||||||
// }
|
if (['.zip', '.rar', '.7z', '.tar', '.gz'].includes(ext)) {
|
||||||
|
return <FileZipFilled />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.txt', '.json', '.log', '.yml', '.yaml', '.xml', '.csv'].includes(ext)) {
|
||||||
|
return <FileTextFilled />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.url'].includes(ext)) {
|
||||||
|
return <LinkOutlined />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.sitemap'].includes(ext)) {
|
||||||
|
return <GlobalOutlined />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['.folder'].includes(ext)) {
|
||||||
|
return <FolderOpenFilled />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FileUnknownFilled />
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileNameRender: FC<{ file: FileType }> = ({ file }) => {
|
||||||
|
const fullName = FileManager.formatFileName(file)
|
||||||
|
const displayName = truncateFileName(fullName)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileName
|
||||||
|
onClick={() => {
|
||||||
|
const path = FileManager.getSafePath(file)
|
||||||
|
if (path) {
|
||||||
|
window.api.file.openPath(path)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={fullName}>
|
||||||
|
{displayName}
|
||||||
|
</FileName>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container style={{ marginTop: 2, marginBottom: 8 }} className="message-attachments">
|
<Container style={{ marginTop: 2, marginBottom: 8 }} className="message-attachments">
|
||||||
<StyledUpload
|
<CustomTag key={block.file.id} icon={getFileIcon(block.file.ext)} color="#37a5aa">
|
||||||
listType="text"
|
<FileNameRender file={block.file} />
|
||||||
disabled
|
</CustomTag>
|
||||||
fileList={[
|
|
||||||
{
|
|
||||||
uid: block.file.id,
|
|
||||||
url: 'file://' + FileManager.getSafePath(block.file),
|
|
||||||
status: 'done' as const,
|
|
||||||
name: FileManager.formatFileName(block.file)
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -89,23 +123,11 @@ const Container = styled.div`
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
`
|
`
|
||||||
|
|
||||||
// const Image = styled(AntdImage)`
|
const FileName = styled.span`
|
||||||
// border-radius: 10px;
|
cursor: pointer;
|
||||||
// `
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
// const ToobarWrapper = styled(Space)`
|
}
|
||||||
// padding: 0px 24px;
|
`
|
||||||
// color: #fff;
|
|
||||||
// font-size: 20px;
|
|
||||||
// background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
// border-radius: 100px;
|
|
||||||
// .anticon {
|
|
||||||
// padding: 12px;
|
|
||||||
// cursor: pointer;
|
|
||||||
// }
|
|
||||||
// .anticon:hover {
|
|
||||||
// opacity: 0.3;
|
|
||||||
// }
|
|
||||||
// `
|
|
||||||
|
|
||||||
export default MessageAttachments
|
export default MessageAttachments
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const MessageContent: React.FC<Props> = ({ message }) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isEmpty(message.mentions) && (
|
{!isEmpty(message.mentions) && (
|
||||||
<Flex gap="8px" wrap>
|
<Flex gap="8px" wrap style={{ marginBottom: 10 }}>
|
||||||
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
|
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ const EditorContainer = styled.div`
|
|||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
border: 0.5px solid var(--color-border);
|
border: 0.5px solid var(--color-border);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
border-radius: 15px;
|
border-radius: var(--list-item-border-radius);
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
background-color: var(--color-background-opacity);
|
background-color: var(--color-background-opacity);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user