diff --git a/package.json b/package.json index 9dad989..b244a4a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gerbil", "productName": "Gerbil", - "version": "0.9.2", + "version": "0.9.3", "description": "Run Large Language Models locally", "main": "out/main/index.js", "homepage": "./", diff --git a/src/App.tsx b/src/App.tsx index 01d4f2a..df5bae0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -147,6 +147,8 @@ export const App = () => { } }; + const isAnyModalOpen = settingsOpened || showEjectModal || showUpdateModal; + return ( { onTabChange={setActiveInterfaceTab} onEject={handleEject} onOpenSettings={() => setSettingsOpened(true)} + isModalOpen={isAnyModalOpen} /> void; onEject?: () => void; onOpenSettings?: () => void; + isModalOpen?: boolean; } export const TitleBar = ({ @@ -35,6 +36,7 @@ export const TitleBar = ({ onTabChange, onEject, onOpenSettings, + isModalOpen = false, }: TitleBarProps) => { const computedColorScheme = useComputedColorScheme('light', { getInitialValueInEffect: false, @@ -97,7 +99,7 @@ export const TitleBar = ({ ? 'var(--mantine-color-dark-7)' : 'var(--mantine-color-gray-0)', borderBottom: '1px solid var(--mantine-color-default-border)', - WebkitAppRegion: 'drag', + WebkitAppRegion: isModalOpen ? 'no-drag' : 'drag', userSelect: 'none', position: 'relative', }} diff --git a/src/components/settings/AboutTab.tsx b/src/components/settings/AboutTab.tsx index 6d2891e..912b783 100644 --- a/src/components/settings/AboutTab.tsx +++ b/src/components/settings/AboutTab.tsx @@ -8,9 +8,10 @@ import { Image, Center, Badge, + Button, rem, } from '@mantine/core'; -import { Github } from 'lucide-react'; +import { Github, FolderOpen } from 'lucide-react'; import type { VersionInfo } from '@/types/electron'; import { PRODUCT_NAME } from '@/constants'; import iconUrl from '/icon.png'; @@ -76,26 +77,48 @@ export const AboutTab = () => { - A desktop app to easily run Large Language Models locally. + Run Large Language Models locally - { - e.preventDefault(); - window.electronAPI.app.openExternal( - 'https://github.com/lone-cloud/gerbil' - ); - }} - style={{ textDecoration: 'none' }} - > - - - - GitHub - - - + + { + e.preventDefault(); + window.electronAPI.app.openExternal( + 'https://github.com/lone-cloud/gerbil' + ); + }} + style={{ textDecoration: 'none' }} + > + + + + GitHub + + + + + + diff --git a/src/components/settings/SettingsModal.tsx b/src/components/settings/SettingsModal.tsx index efbb9f9..251034f 100644 --- a/src/components/settings/SettingsModal.tsx +++ b/src/components/settings/SettingsModal.tsx @@ -169,7 +169,7 @@ export const SettingsModal = ({ }} > diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 49429d9..deb199a 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -171,6 +171,16 @@ export class IPCHandlers { } }); + ipcMain.handle('app:showLogsFolder', async () => { + try { + const logsDir = this.logManager.getLogsDirectory(); + await shell.openPath(logsDir); + return { success: true }; + } catch (error) { + return { success: false, error: (error as Error).message }; + } + }); + ipcMain.handle('app:minimizeWindow', () => { const mainWindow = this.windowManager.getMainWindow(); mainWindow?.minimize(); diff --git a/src/main/managers/WindowManager.ts b/src/main/managers/WindowManager.ts index 829f116..78da6b5 100644 --- a/src/main/managers/WindowManager.ts +++ b/src/main/managers/WindowManager.ts @@ -1,4 +1,12 @@ -import { BrowserWindow, app, shell, nativeImage, screen, Menu } from 'electron'; +import { + BrowserWindow, + app, + shell, + nativeImage, + screen, + Menu, + clipboard, +} from 'electron'; import { join } from 'path'; import { stripVTControlCharacters } from 'util'; import { PRODUCT_NAME } from '../../constants'; @@ -104,40 +112,96 @@ export class WindowManager { this.mainWindow.webContents.on('context-menu', (_, params) => { const hasLinkURL = !!params.linkURL; + const hasSelection = !!params.selectionText; + const isEditable = params.isEditable; const isDev = this.isDevelopment(); - const menuTemplate = [ - ...(isDev - ? [ - { - label: 'Inspect Element', - click: () => { - this.mainWindow?.webContents.inspectElement( - params.x, - params.y - ); - }, - }, - { type: 'separator' as const }, - ] - : []), - { label: 'Cut', role: 'cut' as const }, - { label: 'Copy', role: 'copy' as const }, - { label: 'Paste', role: 'paste' as const }, - ...(hasLinkURL ? [{ type: 'separator' as const }] : []), - { + const canCut = hasSelection && isEditable; + const canCopy = hasSelection; + const canPaste = isEditable; + const canSelectAll = isEditable || params.mediaType === 'none'; + const canUndo = isEditable && params.editFlags?.canUndo; + const canRedo = isEditable && params.editFlags?.canRedo; + const hasEditOperations = + canCut || canCopy || canPaste || canSelectAll || canUndo || canRedo; + + const menuItems = []; + + if (isDev) { + menuItems.push({ + label: 'Inspect Element', + click: () => { + this.mainWindow?.webContents.inspectElement(params.x, params.y); + }, + }); + } + + if (hasEditOperations) { + if (isDev) { + menuItems.push({ type: 'separator' as const }); + } + + if (canUndo) { + menuItems.push({ label: 'Undo', role: 'undo' as const }); + } + if (canRedo) { + menuItems.push({ label: 'Redo', role: 'redo' as const }); + } + + if ( + (canUndo || canRedo) && + (canCut || canCopy || canPaste || canSelectAll) + ) { + menuItems.push({ type: 'separator' as const }); + } + + if (canCut) { + menuItems.push({ label: 'Cut', role: 'cut' as const }); + } + if (canCopy) { + menuItems.push({ label: 'Copy', role: 'copy' as const }); + } + if (canPaste) { + menuItems.push({ label: 'Paste', role: 'paste' as const }); + } + + if ((canCut || canCopy || canPaste) && canSelectAll) { + menuItems.push({ type: 'separator' as const }); + } + + if (canSelectAll) { + menuItems.push({ label: 'Select All', role: 'selectAll' as const }); + } + } + + if (hasLinkURL) { + if (isDev || hasEditOperations) { + menuItems.push({ type: 'separator' as const }); + } + + menuItems.push({ label: 'Open Link in Browser', - visible: hasLinkURL, click: () => { if (params.linkURL) { shell.openExternal(params.linkURL); } }, - }, - ]; + }); - const menu = Menu.buildFromTemplate(menuTemplate); - menu.popup({ window: this.mainWindow! }); + menuItems.push({ + label: 'Copy Link Address', + click: () => { + if (params.linkURL) { + clipboard.writeText(params.linkURL); + } + }, + }); + } + + if (menuItems.length > 0) { + const menu = Menu.buildFromTemplate(menuItems); + menu.popup({ window: this.mainWindow! }); + } }); } diff --git a/src/preload/index.ts b/src/preload/index.ts index 285b39c..228d8e4 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -77,6 +77,7 @@ const koboldAPI: KoboldAPI = { const appAPI: AppAPI = { openExternal: (url) => ipcRenderer.invoke('app:openExternal', url), + showLogsFolder: () => ipcRenderer.invoke('app:showLogsFolder'), getVersion: () => ipcRenderer.invoke('app:getVersion'), getVersionInfo: () => ipcRenderer.invoke('app:getVersionInfo'), minimizeWindow: () => ipcRenderer.invoke('app:minimizeWindow'), diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index c7affc0..e0773da 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -144,6 +144,7 @@ export interface VersionInfo { export interface AppAPI { openExternal: (url: string) => Promise; + showLogsFolder: () => Promise; getVersion: () => Promise; getVersionInfo: () => Promise; minimizeWindow: () => void;