fix: Jan Quick Ask window capture input issues (#4758)

This commit is contained in:
Louis 2025-02-28 16:50:08 +07:00 committed by GitHub
parent b4ba76aa71
commit dda0feb548
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 134 additions and 60 deletions

View File

@ -31,6 +31,7 @@ import { registerGlobalShortcuts } from './utils/shortcut'
import { registerLogger } from './utils/logger' import { registerLogger } from './utils/logger'
const preloadPath = join(__dirname, 'preload.js') const preloadPath = join(__dirname, 'preload.js')
const preloadQuickAskPath = join(__dirname, 'preload.quickask.js')
const rendererPath = join(__dirname, '..', 'renderer') const rendererPath = join(__dirname, '..', 'renderer')
const quickAskPath = join(rendererPath, 'search.html') const quickAskPath = join(rendererPath, 'search.html')
const mainPath = join(rendererPath, 'index.html') const mainPath = join(rendererPath, 'index.html')
@ -133,7 +134,7 @@ function createQuickAskWindow() {
// Feature Toggle for Quick Ask // Feature Toggle for Quick Ask
if (!getAppConfigurations().quick_ask) return if (!getAppConfigurations().quick_ask) return
const startUrl = app.isPackaged ? `file://${quickAskPath}` : quickAskUrl const startUrl = app.isPackaged ? `file://${quickAskPath}` : quickAskUrl
windowManager.createQuickAskWindow(preloadPath, startUrl) windowManager.createQuickAskWindow(preloadQuickAskPath, startUrl)
} }
/** /**

View File

@ -13,10 +13,10 @@ export const quickAskWindowConfig: Electron.BrowserWindowConstructorOptions = {
fullscreenable: false, fullscreenable: false,
resizable: false, resizable: false,
center: true, center: true,
movable: false, movable: true,
maximizable: false, maximizable: false,
focusable: true, focusable: true,
transparent: true, transparent: false,
frame: false, frame: false,
type: 'panel', type: 'panel',
} }

View File

@ -141,6 +141,9 @@ class WindowManager {
return this._quickAskWindow?.isDestroyed() ?? true return this._quickAskWindow?.isDestroyed() ?? true
} }
/**
* Expand the quick ask window
*/
expandQuickAskWindow(heightOffset: number): void { expandQuickAskWindow(heightOffset: number): void {
const width = quickAskWindowConfig.width! const width = quickAskWindowConfig.width!
const height = quickAskWindowConfig.height! + heightOffset const height = quickAskWindowConfig.height! + heightOffset
@ -148,6 +151,9 @@ class WindowManager {
this._quickAskWindow?.setSize(width, height, true) this._quickAskWindow?.setSize(width, height, true)
} }
/**
* Send the selected text to the quick ask window.
*/
sendQuickAskSelectedText(selectedText: string): void { sendQuickAskSelectedText(selectedText: string): void {
this._quickAskWindow?.webContents.send( this._quickAskWindow?.webContents.send(
AppEvent.onSelectedText, AppEvent.onSelectedText,
@ -180,6 +186,9 @@ class WindowManager {
} }
} }
/**
* Clean up all windows.
*/
cleanUp(): void { cleanUp(): void {
if (!this.mainWindow?.isDestroyed()) { if (!this.mainWindow?.isDestroyed()) {
this.mainWindow?.close() this.mainWindow?.close()

View File

@ -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,
})

View File

@ -3,7 +3,7 @@
* @module preload * @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 { contextBridge, ipcRenderer } from 'electron'
import { readdirSync } from 'fs' import { readdirSync } from 'fs'
@ -13,9 +13,8 @@ const interfaces: { [key: string]: (...args: any[]) => any } = {}
APIRoutes.forEach((method) => { APIRoutes.forEach((method) => {
// For each method, create a function on the interfaces object // For each method, create a function on the interfaces object
// This function invokes the method on the ipcRenderer with any provided arguments // This function invokes the method on the ipcRenderer with any provided arguments
interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args) interfaces[method] = (...args: any[]) => ipcRenderer.invoke(method, ...args)
}) })
// Loop over each method in APIEvents // Loop over each method in APIEvents
@ -26,20 +25,21 @@ APIEvents.forEach((method) => {
interfaces[method] = (handler: any) => ipcRenderer.on(method, handler) interfaces[method] = (handler: any) => ipcRenderer.on(method, handler)
}) })
interfaces['changeDataFolder'] = async (path) => {
interfaces['changeDataFolder'] = async path => { const appConfiguration: AppConfiguration = await ipcRenderer.invoke(
const appConfiguration: AppConfiguration = await ipcRenderer.invoke('getAppConfigurations') 'getAppConfigurations'
)
const currentJanDataFolder = appConfiguration.data_folder const currentJanDataFolder = appConfiguration.data_folder
appConfiguration.data_folder = path appConfiguration.data_folder = path
const reflect = require('@alumna/reflect') const reflect = require('@alumna/reflect')
const { err } = await reflect({ const { err } = await reflect({
src: currentJanDataFolder, src: currentJanDataFolder,
dest: path, dest: path,
recursive: true, recursive: true,
delete: false, delete: false,
overwrite: true, overwrite: true,
errorOnExist: false, errorOnExist: false,
}) })
if (err) { if (err) {
console.error(err) console.error(err)
throw err throw err
@ -47,7 +47,7 @@ interfaces['changeDataFolder'] = async path => {
await ipcRenderer.invoke('updateAppConfiguration', appConfiguration) await ipcRenderer.invoke('updateAppConfiguration', appConfiguration)
} }
interfaces['isDirectoryEmpty'] = async path => { interfaces['isDirectoryEmpty'] = async (path) => {
const dirChildren = await readdirSync(path) const dirChildren = await readdirSync(path)
return dirChildren.filter((x) => x !== '.DS_Store').length === 0 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 // This allows the renderer process to access these methods directly
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
...interfaces, ...interfaces,
isQuickAsk: () => false,
}) })

View File

@ -13,10 +13,7 @@ export const metadata: Metadata = {
export default function RootLayout({ children }: PropsWithChildren) { export default function RootLayout({ children }: PropsWithChildren) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body className="h-screen font-sans text-sm antialiased"> <body className="h-screen font-sans text-sm antialiased">{children}</body>
<div className="dragable-bar" />
{children}
</body>
</html> </html>
) )
} }

View File

@ -66,7 +66,7 @@ const UserInput = () => {
<LogoMark width={28} height={28} className="mx-auto" /> <LogoMark width={28} height={28} className="mx-auto" />
<input <input
ref={inputRef} ref={inputRef}
className="flex-1 bg-transparent font-bold focus:outline-none" className="flex-1 bg-transparent font-bold text-[hsla(var(--text-primary))] focus:outline-none"
type="text" type="text"
value={inputValue} value={inputValue}
onChange={handleChange} onChange={handleChange}

View File

@ -8,9 +8,6 @@ import { useSetAtom } from 'jotai'
import ClipboardListener from '@/containers/Providers/ClipboardListener' import ClipboardListener from '@/containers/Providers/ClipboardListener'
import JotaiWrapper from '@/containers/Providers/Jotai'
import ThemeWrapper from '@/containers/Providers/Theme'
import { useLoadTheme } from '@/hooks/useLoadTheme' import { useLoadTheme } from '@/hooks/useLoadTheme'
import { setupCoreServices } from '@/services/coreService' import { setupCoreServices } from '@/services/coreService'
@ -48,15 +45,9 @@ export default function RootLayout() {
useLoadTheme() useLoadTheme()
return ( return (
<html lang="en" suppressHydrationWarning> <>
<body className="font-sans antialiased"> <ClipboardListener />
<JotaiWrapper> <Search />
<ThemeWrapper> </>
<ClipboardListener />
<Search />
</ThemeWrapper>
</JotaiWrapper>
</body>
</html>
) )
} }

View File

@ -5,6 +5,7 @@ import UserInput from './UserInput'
const Search = () => { const Search = () => {
return ( return (
<div className="h-screen w-screen overflow-hidden bg-[hsla(var(--app-bg))]"> <div className="h-screen w-screen overflow-hidden bg-[hsla(var(--app-bg))]">
<div className={'draggable-bar h-[10px]'} />
<UserInput /> <UserInput />
</div> </div>
) )

View File

@ -35,10 +35,7 @@ import useDownloadModel from '@/hooks/useDownloadModel'
import { modelDownloadStateAtom } from '@/hooks/useDownloadState' import { modelDownloadStateAtom } from '@/hooks/useDownloadState'
import { useGetEngines } from '@/hooks/useEngineManagement' import { useGetEngines } from '@/hooks/useEngineManagement'
import { import { useGetFeaturedSources } from '@/hooks/useModelSource'
useGetModelSources,
useGetFeaturedSources,
} from '@/hooks/useModelSource'
import useRecommendedModel from '@/hooks/useRecommendedModel' import useRecommendedModel from '@/hooks/useRecommendedModel'
import useUpdateModelParameters from '@/hooks/useUpdateModelParameters' import useUpdateModelParameters from '@/hooks/useUpdateModelParameters'
@ -91,7 +88,6 @@ const ModelDropdown = ({
const [toggle, setToggle] = useState<HTMLDivElement | null>(null) const [toggle, setToggle] = useState<HTMLDivElement | null>(null)
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom) const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom)
const { recommendedModel, downloadedModels } = useRecommendedModel() const { recommendedModel, downloadedModels } = useRecommendedModel()
const { sources } = useGetModelSources()
const [dropdownOptions, setDropdownOptions] = useState<HTMLDivElement | null>( const [dropdownOptions, setDropdownOptions] = useState<HTMLDivElement | null>(
null null
) )

View File

@ -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}</>}</>
}

View File

@ -14,28 +14,39 @@ import DataLoader from './DataLoader'
import DeepLinkListener from './DeepLinkListener' import DeepLinkListener from './DeepLinkListener'
import KeyListener from './KeyListener' import KeyListener from './KeyListener'
import { QuickAskConfigurator } from './QuickAskConfigurator'
import Responsive from './Responsive' import Responsive from './Responsive'
import SWRConfigProvider from './SWRConfigProvider' import SWRConfigProvider from './SWRConfigProvider'
import SettingsHandler from './SettingsHandler' import SettingsHandler from './SettingsHandler'
const Providers = ({ children }: PropsWithChildren) => { const Providers = ({ children }: PropsWithChildren) => {
const isQuickAsk =
typeof window !== 'undefined' && window.electronAPI?.isQuickAsk()
return ( return (
<SWRConfigProvider> <SWRConfigProvider>
<ThemeWrapper> <ThemeWrapper>
<JotaiWrapper> <JotaiWrapper>
<CoreConfigurator> {isQuickAsk && (
<> <>
<Responsive /> <QuickAskConfigurator> {children} </QuickAskConfigurator>
<KeyListener />
<EventListener />
<DataLoader />
<SettingsHandler />
<DeepLinkListener />
<Toaster />
{children}
</> </>
</CoreConfigurator> )}
{!isQuickAsk && (
<CoreConfigurator>
<>
<Responsive />
<KeyListener />
<EventListener />
<DataLoader />
<SettingsHandler />
<DeepLinkListener />
<Toaster />
<div className={'draggable-bar h-[32px]'} />
{children}
</>
</CoreConfigurator>
)}
</JotaiWrapper> </JotaiWrapper>
</ThemeWrapper> </ThemeWrapper>
</SWRConfigProvider> </SWRConfigProvider>

10
web/hooks/useApp.ts Normal file
View File

@ -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 }
}

View File

@ -43,7 +43,7 @@ export function useGetFeaturedSources() {
const { sources, error, mutate } = useGetModelSources() const { sources, error, mutate } = useGetModelSources()
return { return {
sources: sources?.filter((e) => e.metadata?.tags.includes('featured')), sources: sources?.filter((e) => e.metadata?.tags?.includes('featured')),
error, error,
mutate, mutate,
} }

View File

@ -9,6 +9,8 @@ import Loader from '@/containers/Loader'
export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination' export const SUCCESS_SET_NEW_DESTINATION = 'successSetNewDestination'
import { useApp } from '@/hooks/useApp'
import ModalChangeDirectory, { import ModalChangeDirectory, {
showDirectoryConfirmModalAtom, showDirectoryConfirmModalAtom,
} from './ModalChangeDirectory' } from './ModalChangeDirectory'
@ -32,6 +34,7 @@ const DataFolder = () => {
const [destinationPath, setDestinationPath] = useState(undefined) const [destinationPath, setDestinationPath] = useState(undefined)
const janDataFolderPath = useAtomValue(janDataFolderPathAtom) const janDataFolderPath = useAtomValue(janDataFolderPathAtom)
const { relaunch } = useApp()
const onChangeFolderClick = useCallback(async () => { const onChangeFolderClick = useCallback(async () => {
const destFolder = await window.core?.api?.selectDirectory() const destFolder = await window.core?.api?.selectDirectory()
@ -78,7 +81,7 @@ const DataFolder = () => {
setTimeout(() => { setTimeout(() => {
setShowLoader(false) setShowLoader(false)
}, 1200) }, 1200)
await window.core?.api?.relaunch() await relaunch()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
setShowLoader(false) setShowLoader(false)

View File

@ -13,6 +13,7 @@ import { useDebouncedCallback } from 'use-debounce'
import { toaster } from '@/containers/Toast' import { toaster } from '@/containers/Toast'
import { useApp } from '@/hooks/useApp'
import { useConfigurations } from '@/hooks/useConfigurations' import { useConfigurations } from '@/hooks/useConfigurations'
import ModalDeleteAllThreads from '@/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads' import ModalDeleteAllThreads from '@/screens/Thread/ThreadLeftPanel/ModalDeleteAllThreads'
@ -47,6 +48,7 @@ const Advanced = ({ setSubdir }: { setSubdir: (subdir: string) => void }) => {
const { configurePullOptions } = useConfigurations() const { configurePullOptions } = useConfigurations()
const setModalActionThread = useSetAtom(modalActionThreadAtom) const setModalActionThread = useSetAtom(modalActionThreadAtom)
const { relaunch } = useApp()
/** /**
* There could be a case where the state update is not synced * 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 ( const updateQuickAskEnabled = async (
e: boolean, e: boolean,
relaunch: boolean = true relaunchApp: boolean = true
) => { ) => {
const appConfiguration: AppConfiguration = const appConfiguration: AppConfiguration =
await window.core?.api?.getAppConfigurations() await window.core?.api?.getAppConfigurations()
appConfiguration.quick_ask = e appConfiguration.quick_ask = e
await window.core?.api?.updateAppConfiguration(appConfiguration) 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 // It affects other settings, so we need to reset them
const isRelaunch = quickAskEnabled const isRelaunch = quickAskEnabled
if (quickAskEnabled) await updateQuickAskEnabled(false, false) if (quickAskEnabled) await updateQuickAskEnabled(false, false)
if (isRelaunch) window.core?.api?.relaunch() if (isRelaunch) relaunch()
} }
return ( return (

View File

@ -9,6 +9,8 @@ import { Marked, Renderer } from 'marked'
import Loader from '@/containers/Loader' import Loader from '@/containers/Loader'
import { useApp } from '@/hooks/useApp'
import { formatExtensionsName } from '@/utils/converter' import { formatExtensionsName } from '@/utils/converter'
import { extensionManager } from '@/extension' import { extensionManager } from '@/extension'
@ -23,6 +25,7 @@ const ExtensionCatalog = () => {
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const [showLoading, setShowLoading] = useState(false) const [showLoading, setShowLoading] = useState(false)
const fileInputRef = useRef<HTMLInputElement | null>(null) const fileInputRef = useRef<HTMLInputElement | null>(null)
const { relaunch } = useApp()
useEffect(() => { useEffect(() => {
const getAllSettings = async () => { const getAllSettings = async () => {
@ -74,7 +77,7 @@ const ExtensionCatalog = () => {
// Send the filename of the to be installed extension // Send the filename of the to be installed extension
// to the main process for installation // to the main process for installation
const installed = await extensionManager.install([extensionFile]) 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 // Send the filename of the to be uninstalled extension
// to the main process for removal // to the main process for removal
const res = await extensionManager.uninstall([name]) const res = await extensionManager.uninstall([name])
if (res) window.core?.api?.relaunch() if (res) relaunch()
} }
/** /**

View File

@ -7,18 +7,18 @@
text-decoration: underline; text-decoration: underline;
} }
.dragable-bar { .draggable-bar {
position: absolute; position: absolute;
left: 0px; left: 0px;
top: 0px; top: 0px;
width: 100%; width: 100%;
height: 32px;
user-select: none;
-webkit-app-region: drag; -webkit-app-region: drag;
} }
.unset-drag { .unset-drag {
user-select: inherit;
-webkit-app-region: no-drag; -webkit-app-region: no-drag;
pointer-events: all; /* Ensure it receives input events */
} }
.unselect { .unselect {