diff --git a/docs/content.en/docs/release-notes/_index.md b/docs/content.en/docs/release-notes/_index.md index a5225573..fe99b9cd 100644 --- a/docs/content.en/docs/release-notes/_index.md +++ b/docs/content.en/docs/release-notes/_index.md @@ -20,6 +20,7 @@ Information about release notes of Coco Server is provided here. - feat: add a border to the main window in Windows 10 #343 - feat: mobile terminal adaptation about style #348 - feat: service list popup box supports keyboard-only operation #359 +- feat: networked search data sources support search and keyboard-only operation #367 ### Bug fix @@ -37,6 +38,7 @@ Information about release notes of Coco Server is provided here. ### Breaking changes ### Features + - feat: add web pages components #277 - feat: support for customizing some of the preset shortcuts #316 - feat: support multi websocket connections #314 diff --git a/src-tauri/src/server/datasource.rs b/src-tauri/src/server/datasource.rs index 846d26b6..37259f14 100644 --- a/src-tauri/src/server/datasource.rs +++ b/src-tauri/src/server/datasource.rs @@ -8,6 +8,13 @@ use std::collections::HashMap; use std::sync::{Arc, RwLock}; use tauri::{AppHandle, Runtime}; +#[derive(serde::Deserialize, Debug)] +pub struct GetDatasourcesByServerOptions { + pub from: Option, + pub size: Option, + pub query: Option, +} + lazy_static! { static ref DATASOURCE_CACHE: Arc>>> = Arc::new(RwLock::new(HashMap::new())); @@ -25,7 +32,7 @@ pub fn save_datasource_to_cache(server_id: &str, datasources: Vec) { #[allow(dead_code)] pub fn get_datasources_from_cache(server_id: &str) -> Option> { let cache = DATASOURCE_CACHE.read().unwrap(); // Acquire read lock - // dbg!("cache: {:?}", &cache); + // dbg!("cache: {:?}", &cache); let server_cache = cache.get(server_id)?; // Get the server's cache Some(server_cache.clone()) } @@ -41,7 +48,7 @@ pub async fn refresh_all_datasources(_app_handle: &AppHandle) -> // dbg!("fetch datasources for server: {}", &server.id); // Attempt to get datasources by server, and continue even if it fails - let connectors = match get_datasources_by_server(server.id.as_str()).await { + let connectors = match get_datasources_by_server(server.id.as_str(), None).await { Ok(connectors) => { // Process connectors only after fetching them let connectors_map: HashMap = connectors @@ -83,13 +90,48 @@ pub async fn refresh_all_datasources(_app_handle: &AppHandle) -> } #[tauri::command] -pub async fn get_datasources_by_server(id: &str) -> Result, String> { +pub async fn get_datasources_by_server( + id: &str, + options: Option, +) -> Result, String> { + let from = options.as_ref().and_then(|opt| opt.from).unwrap_or(0); + let size = options.as_ref().and_then(|opt| opt.size).unwrap_or(10000); + let query = options + .and_then(|opt| opt.query) + .unwrap_or(String::default()); + + let mut body = serde_json::json!({ + "from": from, + "size": size, + }); + + if !query.is_empty() { + body["query"] = serde_json::json!({ + "bool": { + "must": [{ + "query_string": { + "fields": ["combined_fulltext"], + "query": query, + "fuzziness": "AUTO", + "fuzzy_prefix_length": 2, + "fuzzy_max_expansions": 10, + "fuzzy_transpositions": true, + "allow_leading_wildcard": false + } + }] + } + }); + } + // Perform the async HTTP request outside the cache lock - let resp = HttpClient::get(id, "/datasource/_search", None) - .await - .map_err(|e| { - format!("Error fetching datasource: {}", e) - })?; + let resp = HttpClient::post( + id, + "/datasource/_search", + None, + Some(reqwest::Body::from(body.to_string())), + ) + .await + .map_err(|e| format!("Error fetching datasource: {}", e))?; // Parse the search results from the response let datasources: Vec = parse_search_results(resp).await.map_err(|e| { diff --git a/src-tauri/src/server/servers.rs b/src-tauri/src/server/servers.rs index ce7c0fc6..222dcaa3 100644 --- a/src-tauri/src/server/servers.rs +++ b/src-tauri/src/server/servers.rs @@ -333,7 +333,7 @@ pub async fn refresh_coco_server_info( //refresh connectors and datasources let _ = fetch_connectors_by_server(&id).await; - let _ = get_datasources_by_server(&id).await; + let _ = get_datasources_by_server(&id, None).await; Ok(server) } diff --git a/src/components/Common/VisibleKey.tsx b/src/components/Common/VisibleKey.tsx index 5fbcc167..482092f3 100644 --- a/src/components/Common/VisibleKey.tsx +++ b/src/components/Common/VisibleKey.tsx @@ -1,15 +1,17 @@ import { useShortcutsStore } from "@/stores/shortcutsStore"; import { useKeyPress } from "ahooks"; +import clsx from "clsx"; import { FC, ReactNode } from "react"; interface VisibleKeyProps { shortcut: string; - children: ReactNode; + children?: ReactNode; + className?: string; onKeypress?: () => void; } const VisibleKey: FC = (props) => { - const { shortcut, children, onKeypress } = props; + const { shortcut, children, className, onKeypress } = props; const modifierKey = useShortcutsStore((state) => { return state.modifierKey; @@ -22,9 +24,26 @@ const VisibleKey: FC = (props) => { onKeypress?.(); }); + const renderShortcut = () => { + if (shortcut === "leftarrow") { + return "←"; + } + + if (shortcut === "rightarrow") { + return "→"; + } + + return shortcut; + }; + return modifierKeyPressed ? ( -
- {shortcut} +
+ {renderShortcut()}
) : ( children diff --git a/src/components/Search/InputBox.tsx b/src/components/Search/InputBox.tsx index c12567c2..5811bf6e 100644 --- a/src/components/Search/InputBox.tsx +++ b/src/components/Search/InputBox.tsx @@ -34,7 +34,14 @@ interface ChatInputProps { isDeepThinkActive: boolean; setIsDeepThinkActive: () => void; isChatPage?: boolean; - getDataSourcesByServer: (serverId: string) => Promise; + getDataSourcesByServer: ( + serverId: string, + options?: { + from?: number; + size?: number; + query?: string; + } + ) => Promise; setupWindowFocusListener: (callback: () => void) => Promise<() => void>; checkScreenPermission: () => Promise; requestScreenPermission: () => void; @@ -98,6 +105,7 @@ export default function ChatInput({ const modifierKeyPressed = useShortcutsStore((state) => { return state.modifierKeyPressed; }); + console.log("modifierKeyPressed", modifierKeyPressed); const modeSwitch = useShortcutsStore((state) => state.modeSwitch); const returnToInput = useShortcutsStore((state) => state.returnToInput); const deepThinking = useShortcutsStore((state) => state.deepThinking); diff --git a/src/components/Search/SearchPopover.tsx b/src/components/Search/SearchPopover.tsx index 7c16df23..6d1317b4 100644 --- a/src/components/Search/SearchPopover.tsx +++ b/src/components/Search/SearchPopover.tsx @@ -1,6 +1,13 @@ -import { useState, useEffect, useCallback, useMemo, useRef } from "react"; -import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; -import { ChevronDownIcon, RefreshCw, Layers, Globe } from "lucide-react"; +import { useState, useEffect, useCallback, useRef } from "react"; +import { Input, Popover, PopoverButton, PopoverPanel } from "@headlessui/react"; +import { + ChevronDownIcon, + RefreshCw, + Layers, + Globe, + ChevronRight, + ChevronLeft, +} from "lucide-react"; import clsx from "clsx"; import { useTranslation } from "react-i18next"; @@ -11,11 +18,19 @@ import { DataSource } from "@/types/commands"; import Checkbox from "@/components/Common/Checkbox"; import { useShortcutsStore } from "@/stores/shortcutsStore"; import VisibleKey from "../Common/VisibleKey"; +import { useDebounce } from "ahooks"; interface SearchPopoverProps { isSearchActive: boolean; setIsSearchActive: () => void; - getDataSourcesByServer: (serverId: string) => Promise; + getDataSourcesByServer: ( + serverId: string, + options?: { + from?: number; + size?: number; + query?: string; + } + ) => Promise; } export default function SearchPopover({ @@ -33,12 +48,22 @@ export default function SearchPopover({ const currentService = useConnectStore((state) => state.currentService); const [showDataSource, setShowDataSource] = useState(false); + const [keyword, setKeyword] = useState(""); + const debouncedKeyword = useDebounce(keyword, { wait: 500 }); const getDataSourceList = useCallback(async () => { try { + setPage(1); + const res: DataSource[] = await getDataSourcesByServer( - currentService?.id + currentService?.id, + { + query: debouncedKeyword, + } ); + + console.log("res111", res); + if (res?.length === 0) { setDataSourceList([]); return; @@ -52,12 +77,13 @@ export default function SearchPopover({ ...res, ] : []; + setDataSourceList(data); } catch (err) { setDataSourceList([]); console.error("get_datasources_by_server", err); } - }, [currentService?.id]); + }, [currentService?.id, debouncedKeyword]); const popoverRef = useRef(null); const buttonRef = useRef(null); @@ -65,6 +91,10 @@ export default function SearchPopover({ const internetSearchScope = useShortcutsStore((state) => { return state.internetSearchScope; }); + const [page, setPage] = useState(1); + const [totalPage, setTotalPage] = useState(0); + const [visibleList, setVisibleList] = useState([]); + const searchInputRef = useRef(null); useEffect(() => { if (!showDataSource) return; @@ -88,39 +118,73 @@ export default function SearchPopover({ useEffect(() => { if (dataSourceList.length > 0) { - onSelectDataSource("all", true, true); + setSourceDataIds(dataSourceList.slice(1).map((item) => item.id)); } }, [dataSourceList]); useEffect(() => { getDataSourceList(); - }, [currentService?.id]); + }, [currentService?.id, debouncedKeyword]); - const memoizedDataSourceIds = useMemo( - () => new Set(sourceDataIds), - [sourceDataIds] - ); + useEffect(() => { + setTotalPage(Math.ceil(dataSourceList.length / 10)); + }, [dataSourceList]); + + useEffect(() => { + if (dataSourceList.length === 0) return; + + const startIndex = (page - 1) * 9; + const endIndex = startIndex + 9; + + const list = [ + dataSourceList[0], + ...dataSourceList.slice(1).slice(startIndex, endIndex), + ]; + + setVisibleList(list); + }, [dataSourceList, page]); const onSelectDataSource = useCallback( (id: string, checked: boolean, isAll: boolean) => { - if (isAll) { - setSourceDataIds( - checked ? dataSourceList.slice(1).map((item) => item.id) : [] - ); - return; + let nextSourceDataIds = new Set(sourceDataIds); + + const ids = isAll ? visibleList.slice(1).map((item) => item.id) : [id]; + + for (const id of ids) { + if (checked) { + nextSourceDataIds.add(id); + } else { + nextSourceDataIds.delete(id); + } } - const updatedIds = new Set(memoizedDataSourceIds); - if (checked) { - updatedIds.add(id); - } else { - updatedIds.delete(id); - } - setSourceDataIds(Array.from(updatedIds)); + setSourceDataIds(Array.from(nextSourceDataIds)); }, - [dataSourceList, memoizedDataSourceIds] + [visibleList, sourceDataIds] ); + const handleRefresh = async () => { + setIsRefreshDataSource(true); + + await getDataSourceList(); + + setTimeout(() => { + setIsRefreshDataSource(false); + }, 1000); + }; + + const handlePrev = () => { + if (page === 1) return; + + setPage(page - 1); + }; + + const handleNext = () => { + if (page === totalPage) return; + + setPage(page + 1); + }; + return (
- {dataSourceList?.length > 0 && ( + {visibleList?.length > 0 && (
{ e.stopPropagation(); }} > -
- {t("search.input.searchPopover.title")} +
+
+ {t("search.input.searchPopover.title")} -
{ - setIsRefreshDataSource(true); +
+ + + +
+
- getDataSourceList(); +
+
+ { + searchInputRef.current?.focus(); + }} + /> +
- setTimeout(() => { - setIsRefreshDataSource(false); - }, 1000); - }} - className="size-[24px] flex justify-center items-center rounded-lg border border-black/10 dark:border-white/10 cursor-pointer" - > - { + setKeyword(e.target.value); + }} />
+ +
    + {visibleList?.map((item, index) => { + const { id, name } = item; + + const isAll = index === 0; + + const isChecked = () => { + if (isAll) { + return visibleList.slice(1).every((item) => { + return sourceDataIds.includes(item.id); + }); + } else { + return sourceDataIds.includes(id); + } + }; + + return ( +
  • +
    + {isAll ? ( + + ) : ( + + )} + + + {isAll && name ? t(name) : name} + +
    + +
    + { + onSelectDataSource(id, !isChecked(), isAll); + }} + /> + +
    + + onSelectDataSource(id, value, isAll) + } + /> +
    +
    +
  • + ); + })} +
-
    - {dataSourceList?.map((item, index) => { - const { id, name } = item; - const isAll = index === 0; +
    + + + - return ( -
  • -
    - {isAll ? ( - - ) : ( - - )} +
    + {page}/{totalPage} +
    - {isAll && name ? t(name) : name} -
    - -
    - - onSelectDataSource(id, value, isAll) - } - /> -
    -
  • - ); - })} -
+ + + +
) : null} diff --git a/src/components/SearchChat/index.tsx b/src/components/SearchChat/index.tsx index f92a95a5..498b5e49 100644 --- a/src/components/SearchChat/index.tsx +++ b/src/components/SearchChat/index.tsx @@ -23,7 +23,10 @@ import { useStartupStore } from "@/stores/startupStore"; import { DataSource } from "@/types/commands"; import { useThemeStore } from "@/stores/themeStore"; import { Get } from "@/api/axiosRequest"; -import { useMount } from "ahooks"; +import { useKeyPress, useMount } from "ahooks"; +import { modifierKeys } from "../Settings/Advanced/components/Shortcuts"; +import { useShortcutsStore } from "@/stores/shortcutsStore"; +import { useModifierKeyPress } from "@/hooks/useModifierKeyPress"; interface SearchChatProps { isTauri?: boolean; @@ -170,10 +173,18 @@ function SearchChat({ }, []); const getDataSourcesByServer = useCallback( - async (serverId: string): Promise => { + async ( + serverId: string, + options?: { + from?: number; + size?: number; + query?: string; + } + ): Promise => { if (isTauri) { return platformAdapter.invokeBackend("get_datasources_by_server", { id: serverId, + options, }); } else { const [error, response]: any = await Get("/datasource/_search"); diff --git a/src/hooks/useModifierKeyPress.ts b/src/hooks/useModifierKeyPress.ts new file mode 100644 index 00000000..335dbd27 --- /dev/null +++ b/src/hooks/useModifierKeyPress.ts @@ -0,0 +1,24 @@ +import { modifierKeys } from "@/components/Settings/Advanced/components/Shortcuts"; +import { useShortcutsStore } from "@/stores/shortcutsStore"; +import { useKeyPress } from "ahooks"; + +export const useModifierKeyPress = () => { + const modifierKey = useShortcutsStore((state) => { + return state.modifierKey; + }); + const setModifierKeyPressed = useShortcutsStore((state) => { + return state.setModifierKeyPressed; + }); + + useKeyPress( + modifierKeys, + (event, key) => { + if (key === modifierKey) { + setModifierKeyPressed(event.type === "keydown"); + } + }, + { + events: ["keydown", "keyup"], + } + ); +}; diff --git a/src/hooks/useSyncStore.ts b/src/hooks/useSyncStore.ts new file mode 100644 index 00000000..faadb242 --- /dev/null +++ b/src/hooks/useSyncStore.ts @@ -0,0 +1,107 @@ +import { useShortcutsStore } from "@/stores/shortcutsStore"; +import { useStartupStore } from "@/stores/startupStore"; +import platformAdapter from "@/utils/platformAdapter"; +import { useEffect } from "react"; + +export const useSyncStore = () => { + const setModifierKey = useShortcutsStore((state) => { + return state.setModifierKey; + }); + const setModeSwitch = useShortcutsStore((state) => { + return state.setModeSwitch; + }); + const setReturnToInput = useShortcutsStore((state) => { + return state.setReturnToInput; + }); + const setVoiceInput = useShortcutsStore((state) => { + return state.setVoiceInput; + }); + const setAddFile = useShortcutsStore((state) => { + return state.setAddFile; + }); + const setDefaultStartupWindow = useStartupStore((state) => { + return state.setDefaultStartupWindow; + }); + const setDefaultContentForSearchWindow = useStartupStore((state) => { + return state.setDefaultContentForSearchWindow; + }); + const setDefaultContentForChatWindow = useStartupStore((state) => { + return state.setDefaultContentForChatWindow; + }); + const setDeepThinking = useShortcutsStore((state) => { + return state.setDeepThinking; + }); + const setInternetSearch = useShortcutsStore((state) => { + return state.setInternetSearch; + }); + const setInternetSearchScope = useShortcutsStore((state) => { + return state.setInternetSearchScope; + }); + const setHistoricalRecords = useShortcutsStore((state) => { + return state.setHistoricalRecords; + }); + const setNewSession = useShortcutsStore((state) => { + return state.setNewSession; + }); + const setFixedWindow = useShortcutsStore((state) => { + return state.setFixedWindow; + }); + const setServiceList = useShortcutsStore((state) => { + return state.setServiceList; + }); + const setExternal = useShortcutsStore((state) => { + return state.setExternal; + }); + + useEffect(() => { + const unListeners = Promise.all([ + platformAdapter.listenEvent("change-shortcuts-store", ({ payload }) => { + const { + modifierKey, + modeSwitch, + returnToInput, + voiceInput, + addFile, + deepThinking, + internetSearch, + internetSearchScope, + historicalRecords, + newSession, + fixedWindow, + serviceList, + external, + } = payload; + setModifierKey(modifierKey); + setModeSwitch(modeSwitch); + setReturnToInput(returnToInput); + setVoiceInput(voiceInput); + setAddFile(addFile); + setDeepThinking(deepThinking); + setInternetSearch(internetSearch); + setInternetSearchScope(internetSearchScope); + setHistoricalRecords(historicalRecords); + setNewSession(newSession); + setFixedWindow(fixedWindow); + setServiceList(serviceList); + setExternal(external); + }), + + platformAdapter.listenEvent("change-startup-store", ({ payload }) => { + const { + defaultStartupWindow, + defaultContentForSearchWindow, + defaultContentForChatWindow, + } = payload; + setDefaultStartupWindow(defaultStartupWindow); + setDefaultContentForSearchWindow(defaultContentForSearchWindow); + setDefaultContentForChatWindow(defaultContentForChatWindow); + }), + ]); + + return () => { + unListeners.then((fns) => { + fns.forEach((fn) => fn()); + }); + }; + }, []); +}; diff --git a/src/pages/chat/index.tsx b/src/pages/chat/index.tsx index 46da3fbb..9cca2640 100644 --- a/src/pages/chat/index.tsx +++ b/src/pages/chat/index.tsx @@ -29,6 +29,7 @@ import { } from "@/commands"; import { DataSource } from "@/types/commands"; import HistoryList from "@/components/Common/HistoryList"; +import { useSyncStore } from "@/hooks/useSyncStore"; interface ChatProps {} @@ -50,6 +51,8 @@ export default function Chat({}: ChatProps) { const isChatPage = true; + useSyncStore(); + useEffect(() => { getChatHistory(); }, [keyword]); diff --git a/src/pages/main/index.tsx b/src/pages/main/index.tsx index b1b4acca..c890add8 100644 --- a/src/pages/main/index.tsx +++ b/src/pages/main/index.tsx @@ -1,12 +1,9 @@ -import { useCallback, useEffect } from "react"; -import { useKeyPress } from "ahooks"; +import { useCallback } from "react"; import SearchChat from "@/components/SearchChat"; import platformAdapter from "@/utils/platformAdapter"; -import { useShortcutsStore } from "@/stores/shortcutsStore"; -import { useStartupStore } from "@/stores/startupStore"; -import { modifierKeys } from "@/components/Settings/Advanced/components/Shortcuts"; import { useAppStore } from "@/stores/appStore"; +import { useSyncStore } from "@/hooks/useSyncStore"; function MainApp() { const setIsTauri = useAppStore((state) => state.setIsTauri); @@ -22,8 +19,8 @@ function MainApp() { queryStrings: { query: input }, } ); - if (!response || typeof response !== 'object') { - throw new Error('Invalid response format'); + if (!response || typeof response !== "object") { + throw new Error("Invalid response format"); } return response; } catch (error) { @@ -56,124 +53,7 @@ function MainApp() { return platformAdapter.hideWindow(); }, []); - const modifierKey = useShortcutsStore((state) => { - return state.modifierKey; - }); - const setModifierKey = useShortcutsStore((state) => { - return state.setModifierKey; - }); - const setModifierKeyPressed = useShortcutsStore((state) => { - return state.setModifierKeyPressed; - }); - const setModeSwitch = useShortcutsStore((state) => { - return state.setModeSwitch; - }); - const setReturnToInput = useShortcutsStore((state) => { - return state.setReturnToInput; - }); - const setVoiceInput = useShortcutsStore((state) => { - return state.setVoiceInput; - }); - const setAddFile = useShortcutsStore((state) => { - return state.setAddFile; - }); - const setDefaultStartupWindow = useStartupStore((state) => { - return state.setDefaultStartupWindow; - }); - const setDefaultContentForSearchWindow = useStartupStore((state) => { - return state.setDefaultContentForSearchWindow; - }); - const setDefaultContentForChatWindow = useStartupStore((state) => { - return state.setDefaultContentForChatWindow; - }); - const setDeepThinking = useShortcutsStore((state) => { - return state.setDeepThinking; - }); - const setInternetSearch = useShortcutsStore((state) => { - return state.setInternetSearch; - }); - const setInternetSearchScope = useShortcutsStore((state) => { - return state.setInternetSearchScope; - }); - const setHistoricalRecords = useShortcutsStore((state) => { - return state.setHistoricalRecords; - }); - const setNewSession = useShortcutsStore((state) => { - return state.setNewSession; - }); - const setFixedWindow = useShortcutsStore((state) => { - return state.setFixedWindow; - }); - const setServiceList = useShortcutsStore((state) => { - return state.setServiceList; - }); - const setExternal = useShortcutsStore((state) => { - return state.setExternal; - }); - - useEffect(() => { - const unListeners = Promise.all([ - platformAdapter.listenEvent("change-shortcuts-store", ({ payload }) => { - const { - modifierKey, - modeSwitch, - returnToInput, - voiceInput, - addFile, - deepThinking, - internetSearch, - internetSearchScope, - historicalRecords, - newSession, - fixedWindow, - serviceList, - external, - } = payload; - setModifierKey(modifierKey); - setModeSwitch(modeSwitch); - setReturnToInput(returnToInput); - setVoiceInput(voiceInput); - setAddFile(addFile); - setDeepThinking(deepThinking); - setInternetSearch(internetSearch); - setInternetSearchScope(internetSearchScope); - setHistoricalRecords(historicalRecords); - setNewSession(newSession); - setFixedWindow(fixedWindow); - setServiceList(serviceList); - setExternal(external); - }), - - platformAdapter.listenEvent("change-startup-store", ({ payload }) => { - const { - defaultStartupWindow, - defaultContentForSearchWindow, - defaultContentForChatWindow, - } = payload; - setDefaultStartupWindow(defaultStartupWindow); - setDefaultContentForSearchWindow(defaultContentForSearchWindow); - setDefaultContentForChatWindow(defaultContentForChatWindow); - }), - ]); - - return () => { - unListeners.then((fns) => { - fns.forEach((fn) => fn()); - }); - }; - }, []); - - useKeyPress( - modifierKeys, - (event, key) => { - if (key === modifierKey) { - setModifierKeyPressed(event.type === "keydown"); - } - }, - { - events: ["keydown", "keyup"], - } - ); + useSyncStore(); return ( ; @@ -51,14 +52,20 @@ function WebApp({ const setIsTauri = useAppStore((state) => state.setIsTauri); const setEndpoint = useAppStore((state) => state.setEndpoint); const setModeSwitch = useShortcutsStore((state) => state.setModeSwitch); + const setInternetSearch = useShortcutsStore((state) => { + return state.setInternetSearch; + }); useEffect(() => { setIsTauri(false); setEndpoint(serverUrl); setModeSwitch("S"); + setInternetSearch("B"); localStorage.setItem("headers", JSON.stringify(headers || {})); }, []); + useModifierKeyPress(); + const query_coco_fusion = useCallback(async (url: string) => { try { const [error, response]: any = await Get(url); @@ -133,12 +140,19 @@ function WebApp({ }} > {isMobile() && ( -
- +
)} diff --git a/src/routes/layout.tsx b/src/routes/layout.tsx index 768dfc3c..93574cad 100644 --- a/src/routes/layout.tsx +++ b/src/routes/layout.tsx @@ -9,6 +9,7 @@ import useSettingsWindow from "@/hooks/useSettingsWindow"; import { useThemeStore } from "@/stores/themeStore"; import platformAdapter from "@/utils/platformAdapter"; import { AppTheme } from "@/types/index"; +import { useModifierKeyPress } from "@/hooks/useModifierKeyPress"; export default function Layout() { const location = useLocation(); @@ -104,5 +105,7 @@ export default function Layout() { event.preventDefault(); }); + useModifierKeyPress(); + return ; }