From dda0feb5481719a9ef2dad5e0837eab9c34ddbff Mon Sep 17 00:00:00 2001 From: Louis Date: Fri, 28 Feb 2025 16:50:08 +0700 Subject: [PATCH] fix: Jan Quick Ask window capture input issues (#4758) --- electron/main.ts | 3 +- electron/managers/quickAskWindowConfig.ts | 4 +-- electron/managers/window.ts | 9 ++++++ electron/preload.quickask.ts | 32 +++++++++++++++++++ electron/preload.ts | 29 +++++++++-------- web/app/layout.tsx | 5 +-- web/app/search/UserInput.tsx | 2 +- web/app/search/layout.tsx | 17 +++------- web/app/search/page.tsx | 1 + web/containers/ModelDropdown/index.tsx | 6 +--- .../Providers/QuickAskConfigurator.tsx | 17 ++++++++++ web/containers/Providers/index.tsx | 31 ++++++++++++------ web/hooks/useApp.ts | 10 ++++++ web/hooks/useModelSource.ts | 2 +- .../Settings/Advanced/DataFolder/index.tsx | 5 ++- web/screens/Settings/Advanced/index.tsx | 8 +++-- web/screens/Settings/CoreExtensions/index.tsx | 7 ++-- web/styles/base/global.scss | 6 ++-- 18 files changed, 134 insertions(+), 60 deletions(-) create mode 100644 electron/preload.quickask.ts create mode 100644 web/containers/Providers/QuickAskConfigurator.tsx create mode 100644 web/hooks/useApp.ts diff --git a/electron/main.ts b/electron/main.ts index 128270bf4..19192bd99 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -31,6 +31,7 @@ import { registerGlobalShortcuts } from './utils/shortcut' import { registerLogger } from './utils/logger' const preloadPath = join(__dirname, 'preload.js') +const preloadQuickAskPath = join(__dirname, 'preload.quickask.js') const rendererPath = join(__dirname, '..', 'renderer') const quickAskPath = join(rendererPath, 'search.html') const mainPath = join(rendererPath, 'index.html') @@ -133,7 +134,7 @@ function createQuickAskWindow() { // Feature Toggle for Quick Ask if (!getAppConfigurations().quick_ask) return const startUrl = app.isPackaged ? `file://${quickAskPath}` : quickAskUrl - windowManager.createQuickAskWindow(preloadPath, startUrl) + windowManager.createQuickAskWindow(preloadQuickAskPath, startUrl) } /** diff --git a/electron/managers/quickAskWindowConfig.ts b/electron/managers/quickAskWindowConfig.ts index eb30e8ebc..93180dd07 100644 --- a/electron/managers/quickAskWindowConfig.ts +++ b/electron/managers/quickAskWindowConfig.ts @@ -13,10 +13,10 @@ export const quickAskWindowConfig: Electron.BrowserWindowConstructorOptions = { fullscreenable: false, resizable: false, center: true, - movable: false, + movable: true, maximizable: false, focusable: true, - transparent: true, + transparent: false, frame: false, type: 'panel', } diff --git a/electron/managers/window.ts b/electron/managers/window.ts index 918036365..dbb3a5101 100644 --- a/electron/managers/window.ts +++ b/electron/managers/window.ts @@ -141,6 +141,9 @@ class WindowManager { return this._quickAskWindow?.isDestroyed() ?? true } + /** + * Expand the quick ask window + */ expandQuickAskWindow(heightOffset: number): void { const width = quickAskWindowConfig.width! const height = quickAskWindowConfig.height! + heightOffset @@ -148,6 +151,9 @@ class WindowManager { this._quickAskWindow?.setSize(width, height, true) } + /** + * Send the selected text to the quick ask window. + */ sendQuickAskSelectedText(selectedText: string): void { this._quickAskWindow?.webContents.send( AppEvent.onSelectedText, @@ -180,6 +186,9 @@ class WindowManager { } } + /** + * Clean up all windows. + */ cleanUp(): void { if (!this.mainWindow?.isDestroyed()) { this.mainWindow?.close() diff --git a/electron/preload.quickask.ts b/electron/preload.quickask.ts new file mode 100644 index 000000000..7c2cadeb6 --- /dev/null +++ b/electron/preload.quickask.ts @@ -0,0 +1,32 @@ +/** + * Exposes a set of APIs to the renderer process via the contextBridge object. + * @module preload + */ + +import { APIEvents, APIRoutes } from '@janhq/core/node' +import { contextBridge, ipcRenderer } from 'electron' + +const interfaces: { [key: string]: (...args: any[]) => any } = {} + +// Loop over each route in APIRoutes +APIRoutes.forEach((method) => { + // For each method, create a function on the interfaces object + // This function invokes the method on the ipcRenderer with any provided arguments + + interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args) +}) + +// Loop over each method in APIEvents +APIEvents.forEach((method) => { + // For each method, create a function on the interfaces object + // This function sets up an event listener on the ipcRenderer for the method + // The handler for the event is provided as an argument to the function + interfaces[method] = (handler: any) => ipcRenderer.on(method, handler) +}) + +// Expose the 'interfaces' object in the main world under the name 'electronAPI' +// This allows the renderer process to access these methods directly +contextBridge.exposeInMainWorld('electronAPI', { + ...interfaces, + isQuickAsk: () => true, +}) diff --git a/electron/preload.ts b/electron/preload.ts index 05f48d37a..dbfcd1f1e 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -3,7 +3,7 @@ * @module preload */ -import { APIEvents, APIRoutes, AppConfiguration, getAppConfigurations, updateAppConfiguration } from '@janhq/core/node' +import { APIEvents, APIRoutes, AppConfiguration } from '@janhq/core/node' import { contextBridge, ipcRenderer } from 'electron' import { readdirSync } from 'fs' @@ -13,9 +13,8 @@ const interfaces: { [key: string]: (...args: any[]) => any } = {} APIRoutes.forEach((method) => { // For each method, create a function on the interfaces object // This function invokes the method on the ipcRenderer with any provided arguments - + interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args) - }) // Loop over each method in APIEvents @@ -26,20 +25,21 @@ APIEvents.forEach((method) => { interfaces[method] = (handler: any) => ipcRenderer.on(method, handler) }) - -interfaces['changeDataFolder'] = async path => { - const appConfiguration: AppConfiguration = await ipcRenderer.invoke('getAppConfigurations') +interfaces['changeDataFolder'] = async (path) => { + const appConfiguration: AppConfiguration = await ipcRenderer.invoke( + 'getAppConfigurations' + ) const currentJanDataFolder = appConfiguration.data_folder appConfiguration.data_folder = path const reflect = require('@alumna/reflect') const { err } = await reflect({ - src: currentJanDataFolder, - dest: path, - recursive: true, - delete: false, - overwrite: true, - errorOnExist: false, - }) + src: currentJanDataFolder, + dest: path, + recursive: true, + delete: false, + overwrite: true, + errorOnExist: false, + }) if (err) { console.error(err) throw err @@ -47,7 +47,7 @@ interfaces['changeDataFolder'] = async path => { await ipcRenderer.invoke('updateAppConfiguration', appConfiguration) } -interfaces['isDirectoryEmpty'] = async path => { +interfaces['isDirectoryEmpty'] = async (path) => { const dirChildren = await readdirSync(path) return dirChildren.filter((x) => x !== '.DS_Store').length === 0 } @@ -56,4 +56,5 @@ interfaces['isDirectoryEmpty'] = async path => { // This allows the renderer process to access these methods directly contextBridge.exposeInMainWorld('electronAPI', { ...interfaces, + isQuickAsk: () => false, }) diff --git a/web/app/layout.tsx b/web/app/layout.tsx index 5f14d6f5c..aaa905a49 100644 --- a/web/app/layout.tsx +++ b/web/app/layout.tsx @@ -13,10 +13,7 @@ export const metadata: Metadata = { export default function RootLayout({ children }: PropsWithChildren) { return ( - -
- {children} - + {children} ) } diff --git a/web/app/search/UserInput.tsx b/web/app/search/UserInput.tsx index cec694c90..fcabc8ea4 100644 --- a/web/app/search/UserInput.tsx +++ b/web/app/search/UserInput.tsx @@ -66,7 +66,7 @@ const UserInput = () => { - - - - - - - - - + <> + + + ) } diff --git a/web/app/search/page.tsx b/web/app/search/page.tsx index 947999e62..51cf04549 100644 --- a/web/app/search/page.tsx +++ b/web/app/search/page.tsx @@ -5,6 +5,7 @@ import UserInput from './UserInput' const Search = () => { return (
+
) diff --git a/web/containers/ModelDropdown/index.tsx b/web/containers/ModelDropdown/index.tsx index f89dbace4..8fb925e95 100644 --- a/web/containers/ModelDropdown/index.tsx +++ b/web/containers/ModelDropdown/index.tsx @@ -35,10 +35,7 @@ import useDownloadModel from '@/hooks/useDownloadModel' import { modelDownloadStateAtom } from '@/hooks/useDownloadState' import { useGetEngines } from '@/hooks/useEngineManagement' -import { - useGetModelSources, - useGetFeaturedSources, -} from '@/hooks/useModelSource' +import { useGetFeaturedSources } from '@/hooks/useModelSource' import useRecommendedModel from '@/hooks/useRecommendedModel' import useUpdateModelParameters from '@/hooks/useUpdateModelParameters' @@ -91,7 +88,6 @@ const ModelDropdown = ({ const [toggle, setToggle] = useState(null) const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom) const { recommendedModel, downloadedModels } = useRecommendedModel() - const { sources } = useGetModelSources() const [dropdownOptions, setDropdownOptions] = useState( null ) diff --git a/web/containers/Providers/QuickAskConfigurator.tsx b/web/containers/Providers/QuickAskConfigurator.tsx new file mode 100644 index 000000000..40e5caf8e --- /dev/null +++ b/web/containers/Providers/QuickAskConfigurator.tsx @@ -0,0 +1,17 @@ +'use client' + +import { PropsWithChildren, useEffect, useState } from 'react' + +import { setupCoreServices } from '@/services/coreService' + +export const QuickAskConfigurator = ({ children }: PropsWithChildren) => { + const [setupCore, setSetupCore] = useState(false) + + // Services Setup + useEffect(() => { + setupCoreServices() + setSetupCore(true) + }, []) + + return <>{setupCore && <>{children}} +} diff --git a/web/containers/Providers/index.tsx b/web/containers/Providers/index.tsx index 5d14ea95a..78cb76d78 100644 --- a/web/containers/Providers/index.tsx +++ b/web/containers/Providers/index.tsx @@ -14,28 +14,39 @@ import DataLoader from './DataLoader' import DeepLinkListener from './DeepLinkListener' import KeyListener from './KeyListener' +import { QuickAskConfigurator } from './QuickAskConfigurator' import Responsive from './Responsive' import SWRConfigProvider from './SWRConfigProvider' import SettingsHandler from './SettingsHandler' const Providers = ({ children }: PropsWithChildren) => { + const isQuickAsk = + typeof window !== 'undefined' && window.electronAPI?.isQuickAsk() return ( - + {isQuickAsk && ( <> - - - - - - - - {children} + {children} - + )} + {!isQuickAsk && ( + + <> + + + + + + + +
+ {children} + + + )} diff --git a/web/hooks/useApp.ts b/web/hooks/useApp.ts new file mode 100644 index 000000000..f30b9e3c5 --- /dev/null +++ b/web/hooks/useApp.ts @@ -0,0 +1,10 @@ +import { extensionManager } from '@/extension' + +export function useApp() { + async function relaunch() { + const extensions = extensionManager.getAll() + await Promise.all(extensions.map((e) => e.onUnload())) + window.core?.api?.relaunch() + } + return { relaunch } +} diff --git a/web/hooks/useModelSource.ts b/web/hooks/useModelSource.ts index 6f302c2f2..f9e01802a 100644 --- a/web/hooks/useModelSource.ts +++ b/web/hooks/useModelSource.ts @@ -43,7 +43,7 @@ export function useGetFeaturedSources() { const { sources, error, mutate } = useGetModelSources() return { - sources: sources?.filter((e) => e.metadata?.tags.includes('featured')), + sources: sources?.filter((e) => e.metadata?.tags?.includes('featured')), error, mutate, } diff --git a/web/screens/Settings/Advanced/DataFolder/index.tsx b/web/screens/Settings/Advanced/DataFolder/index.tsx index 985dc65c3..f37d6e814 100644 --- a/web/screens/Settings/Advanced/DataFolder/index.tsx +++ b/web/screens/Settings/Advanced/DataFolder/index.tsx @@ -9,6 +9,8 @@ import Loader from '@/containers/Loader' export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination' +import { useApp } from '@/hooks/useApp' + import ModalChangeDirectory, { showDirectoryConfirmModalAtom, } from './ModalChangeDirectory' @@ -32,6 +34,7 @@ const DataFolder = () => { const [destinationPath, setDestinationPath] = useState(undefined) const janDataFolderPath = useAtomValue(janDataFolderPathAtom) + const { relaunch } = useApp() const onChangeFolderClick = useCallback(async () => { const destFolder = await window.core?.api?.selectDirectory() @@ -78,7 +81,7 @@ const DataFolder = () => { setTimeout(() => { setShowLoader(false) }, 1200) - await window.core?.api?.relaunch() + await relaunch() } catch (e) { console.error(e) setShowLoader(false) diff --git a/web/screens/Settings/Advanced/index.tsx b/web/screens/Settings/Advanced/index.tsx index 9cb940171..2dd0dfb0d 100644 --- a/web/screens/Settings/Advanced/index.tsx +++ b/web/screens/Settings/Advanced/index.tsx @@ -13,6 +13,7 @@ import { useDebouncedCallback } from 'use-debounce' import { toaster } from '@/containers/Toast' +import { useApp } from '@/hooks/useApp' import { useConfigurations } from '@/hooks/useConfigurations' import ModalDeleteAllThreads from '@/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads' @@ -47,6 +48,7 @@ const Advanced = ({ setSubdir }: { setSubdir: (subdir: string) => void }) => { const { configurePullOptions } = useConfigurations() const setModalActionThread = useSetAtom(modalActionThreadAtom) + const { relaunch } = useApp() /** * There could be a case where the state update is not synced @@ -66,13 +68,13 @@ const Advanced = ({ setSubdir }: { setSubdir: (subdir: string) => void }) => { */ const updateQuickAskEnabled = async ( e: boolean, - relaunch: boolean = true + relaunchApp: boolean = true ) => { const appConfiguration: AppConfiguration = await window.core?.api?.getAppConfigurations() appConfiguration.quick_ask = e await window.core?.api?.updateAppConfiguration(appConfiguration) - if (relaunch) window.core?.api?.relaunch() + if (relaunchApp) relaunch() } /** @@ -92,7 +94,7 @@ const Advanced = ({ setSubdir }: { setSubdir: (subdir: string) => void }) => { // It affects other settings, so we need to reset them const isRelaunch = quickAskEnabled if (quickAskEnabled) await updateQuickAskEnabled(false, false) - if (isRelaunch) window.core?.api?.relaunch() + if (isRelaunch) relaunch() } return ( diff --git a/web/screens/Settings/CoreExtensions/index.tsx b/web/screens/Settings/CoreExtensions/index.tsx index 030df24c2..4252c877c 100644 --- a/web/screens/Settings/CoreExtensions/index.tsx +++ b/web/screens/Settings/CoreExtensions/index.tsx @@ -9,6 +9,8 @@ import { Marked, Renderer } from 'marked' import Loader from '@/containers/Loader' +import { useApp } from '@/hooks/useApp' + import { formatExtensionsName } from '@/utils/converter' import { extensionManager } from '@/extension' @@ -23,6 +25,7 @@ const ExtensionCatalog = () => { const [searchText, setSearchText] = useState('') const [showLoading, setShowLoading] = useState(false) const fileInputRef = useRef(null) + const { relaunch } = useApp() useEffect(() => { const getAllSettings = async () => { @@ -74,7 +77,7 @@ const ExtensionCatalog = () => { // Send the filename of the to be installed extension // to the main process for installation const installed = await extensionManager.install([extensionFile]) - if (installed) window.core?.api?.relaunch() + if (installed) relaunch() } /** @@ -87,7 +90,7 @@ const ExtensionCatalog = () => { // Send the filename of the to be uninstalled extension // to the main process for removal const res = await extensionManager.uninstall([name]) - if (res) window.core?.api?.relaunch() + if (res) relaunch() } /** diff --git a/web/styles/base/global.scss b/web/styles/base/global.scss index 7f71e7ff6..77f64c267 100644 --- a/web/styles/base/global.scss +++ b/web/styles/base/global.scss @@ -7,18 +7,18 @@ text-decoration: underline; } - .dragable-bar { + .draggable-bar { position: absolute; left: 0px; top: 0px; width: 100%; - height: 32px; - user-select: none; -webkit-app-region: drag; } .unset-drag { + user-select: inherit; -webkit-app-region: no-drag; + pointer-events: all; /* Ensure it receives input events */ } .unselect {