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'
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)
}
/**

View File

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

View File

@ -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()

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
*/
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,
})

View File

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

View File

@ -66,7 +66,7 @@ const UserInput = () => {
<LogoMark width={28} height={28} className="mx-auto" />
<input
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"
value={inputValue}
onChange={handleChange}

View File

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

View File

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

View File

@ -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<HTMLDivElement | null>(null)
const [selectedModel, setSelectedModel] = useAtom(selectedModelAtom)
const { recommendedModel, downloadedModels } = useRecommendedModel()
const { sources } = useGetModelSources()
const [dropdownOptions, setDropdownOptions] = useState<HTMLDivElement | 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 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 (
<SWRConfigProvider>
<ThemeWrapper>
<JotaiWrapper>
<CoreConfigurator>
{isQuickAsk && (
<>
<Responsive />
<KeyListener />
<EventListener />
<DataLoader />
<SettingsHandler />
<DeepLinkListener />
<Toaster />
{children}
<QuickAskConfigurator> {children} </QuickAskConfigurator>
</>
</CoreConfigurator>
)}
{!isQuickAsk && (
<CoreConfigurator>
<>
<Responsive />
<KeyListener />
<EventListener />
<DataLoader />
<SettingsHandler />
<DeepLinkListener />
<Toaster />
<div className={'draggable-bar h-[32px]'} />
{children}
</>
</CoreConfigurator>
)}
</JotaiWrapper>
</ThemeWrapper>
</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()
return {
sources: sources?.filter((e) => e.metadata?.tags.includes('featured')),
sources: sources?.filter((e) => e.metadata?.tags?.includes('featured')),
error,
mutate,
}

View File

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

View File

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

View File

@ -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<HTMLInputElement | null>(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()
}
/**

View File

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