From f37d83e2c1a9442f29d63a4b8d3582b638efbc36 Mon Sep 17 00:00:00 2001 From: chessjoe Date: Sun, 26 Jan 2025 20:09:11 +0800 Subject: [PATCH] feat(shortcuts): add support for disabling keyboard shortcuts on specific sites - Introduced BlockConfig utility to manage shortcut disabling for specific websites - Added SHORTCUT_DISABLED_PAGES configuration in preferences - Implemented shortcut blocking mechanism with URL pattern matching - Enhanced AskPanel and background script to handle shortcut-specific behaviors - Added Notification component for user feedback on disabled shortcuts --- src/assets/conf/preferences.toml | 16 ++++++- src/components/ask-panel.tsx | 23 +++++++++- src/components/base/Notification.tsx | 30 +++++++++++++ src/components/icons.tsx | 4 +- src/pages/background/index.ts | 23 +++++----- src/pages/content/ui/app.tsx | 64 +++++++++++++++++++++++++--- src/types.ts | 1 + src/utils/BlockConfig.ts | 64 ++++++++++++++++++++++++++++ src/utils/StorageManager.ts | 6 +++ 9 files changed, 208 insertions(+), 23 deletions(-) create mode 100644 src/components/base/Notification.tsx create mode 100644 src/utils/BlockConfig.ts diff --git a/src/assets/conf/preferences.toml b/src/assets/conf/preferences.toml index c92c462..67aab59 100644 --- a/src/assets/conf/preferences.toml +++ b/src/assets/conf/preferences.toml @@ -37,5 +37,19 @@ ASK_BUTTON_BLOCK_PAGE = [ "docs.google.com", "drive.google.com", "feishu.cn", - "feishu.com" + "feishu.com", + "feishu.cn/wiki/*", +] + +#----------------------------------------------- +# Keyboard Shortcut Settings +#----------------------------------------------- +# SHORTCUT_DISABLED_PAGES: List of URLs or URL patterns where keyboard shortcuts +# should be disabled to avoid conflicts with website's own shortcuts. +# This is particularly useful for document editing websites. +# Format: Same as ASK_BUTTON_BLOCK_PAGE above +SHORTCUT_DISABLED_PAGES = [ + # Feishu blocks keyboard shortcuts to avoid conflicts with document editing + "feishu.cn", + "feishu.cn/wiki/*", ] diff --git a/src/components/ask-panel.tsx b/src/components/ask-panel.tsx index 77c765c..9f8fbdb 100644 --- a/src/components/ask-panel.tsx +++ b/src/components/ask-panel.tsx @@ -23,6 +23,7 @@ import { StorageManager } from '../utils/StorageManager'; import { Handlebars } from '../../third-party/kbn-handlebars/src/handlebars'; import { SCROLLBAR_STYLES_HIDDEN_X } from '../styles/common'; import { HumanMessage } from '@langchain/core/messages'; +import { BlockConfig } from '../utils/BlockConfig'; interface AskPanelProps extends React.HTMLAttributes { code: string; @@ -397,6 +398,17 @@ function AskPanel(props: AskPanelProps) { setUserInput(''); }; + const blockConfigRef = useRef(null); + + // 初始化配置 + useEffect(() => { + const initBlockConfig = async () => { + blockConfigRef.current = BlockConfig.getInstance(); + await blockConfigRef.current.initialize(); + }; + initBlockConfig(); + }, []); + // myObject.test('你是谁'); // console.log('history = ' + JSON.stringify(history)); return ( @@ -427,7 +439,16 @@ function AskPanel(props: AskPanelProps) { {...rest}>
- Askman {' '} + Askman{' '} + {blockConfigRef.current?.isShortcutDisabled(window.location.href) ? ( + +
+ This shortcut will not work on this site to avoid conflicts with website's functionality. +
+
+ ) : ( + + )}{' '} void; +} + +export default function Notification({ message, duration = 3000, onClose }: NotificationProps) { + const [isVisible, setIsVisible] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(false); + onClose?.(); + }, duration); + + return () => clearTimeout(timer); + }, [duration, onClose]); + + if (!isVisible) return null; + + return ( +
+ {message} +
+ ); +} diff --git a/src/components/icons.tsx b/src/components/icons.tsx index 6064a83..8dab2a3 100644 --- a/src/components/icons.tsx +++ b/src/components/icons.tsx @@ -2,14 +2,16 @@ interface KeyBindingProps { text: string; className?: string; onClick?: () => void; + children?: React.ReactNode; } -function KeyBinding({ text, className = '', onClick }: KeyBindingProps) { +function KeyBinding({ text, className = '', onClick, children }: KeyBindingProps) { return ( {text} + {children} ); } diff --git a/src/pages/background/index.ts b/src/pages/background/index.ts index c1d289b..94410d9 100644 --- a/src/pages/background/index.ts +++ b/src/pages/background/index.ts @@ -34,22 +34,19 @@ function onCommandMessageListener(command) { console.log('background received message', command); switch (command) { case 'ChatPopupDisplay': - chrome.tabs.query({ active: true, lastFocusedWindow: true }, ([tab]) => { + chrome.tabs.query({ active: true, lastFocusedWindow: true }, async ([tab]) => { if (chrome.runtime.lastError) console.error(chrome.runtime.lastError); - // `tab` will either be a `tabs.Tab` instance or `undefined`. if (tab) { - chrome.tabs - .sendMessage(tab.id, { cmd: CommandType.ChatPopupDisplay, pageUrl: tab.url }) - .catch(error => { - console.error(error); - // TODO fix 弹不出来 - // chrome.notifications.create( - // 'basic', { - // iconUrl: '/icon-128.png', type: 'basic', - // message: '请刷新页面后重试', title: error.message - // } - // ) + try { + // 快捷键触发,设置 fromShortcut 为 true + chrome.tabs.sendMessage(tab.id, { + cmd: CommandType.ChatPopupDisplay, + pageUrl: tab.url, + fromShortcut: true, // 标记为快捷键触发 }); + } catch (error) { + console.error('Failed to handle command:', error); + } } }); break; diff --git a/src/pages/content/ui/app.tsx b/src/pages/content/ui/app.tsx index 195399e..8f1f9be 100644 --- a/src/pages/content/ui/app.tsx +++ b/src/pages/content/ui/app.tsx @@ -11,19 +11,51 @@ import { createPortal } from 'react-dom'; import { PageStackoverflowAgent } from '@root/src/agents/page-stackoverflow/script'; import PageStackoverflowToolDropdown from '@root/src/agents/page-stackoverflow/component'; import { StorageManager } from '@src/utils/StorageManager'; +import { BlockConfig } from '@src/utils/BlockConfig'; +import Notification from '@src/components/base/Notification'; const ASK_BUTTON_OFFSET_X = 5; // 按钮距离左侧的偏移量 const tabChatContext = new ChatCoreContext(); +// 模拟键盘事件 +function simulateKeyPress(key: string, ctrlKey = false, metaKey = false) { + const event = new KeyboardEvent('keydown', { + key, + code: 'Key' + key.toUpperCase(), + keyCode: key.toUpperCase().charCodeAt(0), + which: key.toUpperCase().charCodeAt(0), + ctrlKey, + metaKey, + bubbles: true, + composed: true, // 允许事件穿过 Shadow DOM 边界 + cancelable: true, + }); + + // 直接向活动元素发送事件 + const target = document.activeElement || document.body; + target.dispatchEvent(event); +} + export default function App() { const [askButtonVisible, setAskButtonVisible] = useState(false); const [askPanelVisible, setAskPanelVisible] = useState(false); + const [showDoubleClickHint, setShowDoubleClickHint] = useState(false); const targetDom = useRef(null); const [pageActionButton, setPageActionButton] = useState(null); const [preferences, setPreferences] = useState({ ASK_BUTTON: false, ASK_BUTTON_BLOCK_PAGE: [] }); const [askPanelQuotes, setAskPanelQuotes] = useState[]>([]); const [parentRect, setParentRect] = useState(); + const blockConfig = useRef(null); + + // 初始化配置 + useEffect(() => { + const initBlockConfig = async () => { + blockConfig.current = BlockConfig.getInstance(); + await blockConfig.current.initialize(); + }; + initBlockConfig(); + }, []); // 加载用户偏好设置 useEffect(() => { @@ -36,7 +68,7 @@ export default function App() { const isCurrentPageBlocked = () => { const currentUrl = window.location.href; const currentDomain = window.location.hostname; - + return preferences.ASK_BUTTON_BLOCK_PAGE.some(pattern => { // 检查完整 URL 匹配 if (pattern.startsWith('http') && currentUrl.startsWith(pattern)) { @@ -66,7 +98,7 @@ export default function App() { const domEl: HTMLElement = (e.target as HTMLElement).closest('pre'); const highlightEl: HTMLElement = (e.target as HTMLElement).closest('div.highlight'); const btnEl: HTMLElement = (e.target as HTMLElement).closest('#askman-chrome-extension-content-view-root'); - + if (domEl?.tagName === 'PRE' || domEl?.contentEditable === 'true' || highlightEl) { if (domEl) targetDom.current = domEl; else if (highlightEl) targetDom.current = highlightEl; @@ -126,14 +158,24 @@ export default function App() { const onBackgroundMessage = function (message: TabMessage, _sender, _sendResponse) { if (message.cmd === CommandType.ChatPopupDisplay) { + const currentUrl = window.location.href; + const fromShortcut = message.fromShortcut || false; + + // 在黑名单中的网站,快捷键触发时才需要特殊处理 + if (fromShortcut && blockConfig.current?.isShortcutDisabled(currentUrl)) { + // 只有快捷键触发时才发送模拟按键 + const isMac = navigator.platform.includes('Mac'); + simulateKeyPress('i', !isMac, isMac); + return; + } + + // 其他情况(鼠标点击或非黑名单网站)正常显示对话框 if (askPanelVisible) { setAskPanelVisible(false); - return; + } else { + const selection = document.getSelection()?.toString().trim() || ''; + showChat(selection); } - console.log('onBackgroundMessage:', message); - const selection = document.getSelection()?.toString().trim() || ''; - showChat(selection); - return; } }; @@ -180,6 +222,14 @@ export default function App() { return ( <> + {!askPanelVisible && showDoubleClickHint && ( + { + setShowDoubleClickHint(false); + }} + /> + )} {PageStackoverflowAgent.isSupport(window.location.href) && pageActionButton && createPortal( diff --git a/src/types.ts b/src/types.ts index ed0a869..58b8f4d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -52,6 +52,7 @@ export interface TabMessage { cmd: CommandType; selectionText?: string; // tab 内选择的文字 pageUrl?: string; // tab 对应的 url + fromShortcut?: boolean; // 是否由快捷键触发 linkText?: string; // 被点击的链接文本 linkUrl?: string; // 被点击的链接URL } diff --git a/src/utils/BlockConfig.ts b/src/utils/BlockConfig.ts new file mode 100644 index 0000000..e5b7a82 --- /dev/null +++ b/src/utils/BlockConfig.ts @@ -0,0 +1,64 @@ +import { StorageManager } from './StorageManager'; + +export class BlockConfig { + private static instance: BlockConfig; + private patterns: string[] = []; + + private constructor() {} + + public static getInstance(): BlockConfig { + if (!BlockConfig.instance) { + BlockConfig.instance = new BlockConfig(); + } + return BlockConfig.instance; + } + + public async initialize(): Promise { + try { + const preferences = await StorageManager.getUserPreferences(); + // 从用户配置中读取禁用快捷键的页面列表 + this.patterns = preferences.SHORTCUT_DISABLED_PAGES; + } catch (error) { + console.error('Failed to initialize BlockConfig:', error); + // 使用默认模式 + this.patterns = [ + 'feishu.cn', // 默认在飞书页面禁用快捷键 + ]; + } + } + + public isShortcutDisabled(url: string): boolean { + if (!url) return false; + const currentDomain = new window.URL(url).hostname; + + return this.patterns.some(pattern => { + // 检查完整 URL 匹配 + if (pattern.startsWith('http') && url.startsWith(pattern)) { + return true; + } + // 检查域名匹配 + if (!pattern.includes('/') && currentDomain.includes(pattern)) { + return true; + } + // 检查通配符模式 + if (pattern.includes('*')) { + const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$'); + return regex.test(url); + } + return false; + }); + } + + public async savePatterns(patterns: string[]): Promise { + this.patterns = patterns; + const preferences = await StorageManager.getUserPreferences(); + await StorageManager.saveUserPreferences({ + ...preferences, + SHORTCUT_DISABLED_PAGES: patterns, + }); + } + + public getPatterns(): string[] { + return this.patterns; + } +} diff --git a/src/utils/StorageManager.ts b/src/utils/StorageManager.ts index a2fa71a..8d2dec7 100644 --- a/src/utils/StorageManager.ts +++ b/src/utils/StorageManager.ts @@ -26,6 +26,7 @@ export interface UserPreferences { USER_LANGUAGE: string; ASK_BUTTON: boolean; ASK_BUTTON_BLOCK_PAGE: string[]; + SHORTCUT_DISABLED_PAGES: string[]; // 在这些页面上禁用快捷键以避免冲突 } export interface SystemPromptContent { @@ -123,6 +124,7 @@ export const StorageManager = { USER_LANGUAGE: parsedConfig.USER_LANGUAGE as string, ASK_BUTTON: parsedConfig.ASK_BUTTON as boolean, ASK_BUTTON_BLOCK_PAGE: parsedConfig.ASK_BUTTON_BLOCK_PAGE as string[], + SHORTCUT_DISABLED_PAGES: parsedConfig.SHORTCUT_DISABLED_PAGES as string[], }; // logger.debug('Default preferences:', defaultPreferences); @@ -137,6 +139,9 @@ export const StorageManager = { ...(preferences.ASK_BUTTON_BLOCK_PAGE !== undefined && { ASK_BUTTON_BLOCK_PAGE: preferences.ASK_BUTTON_BLOCK_PAGE, }), + ...(preferences.SHORTCUT_DISABLED_PAGES !== undefined && { + SHORTCUT_DISABLED_PAGES: preferences.SHORTCUT_DISABLED_PAGES, + }), }; // logger.debug('Merged preferences:', mergedPreferences); return mergedPreferences; @@ -148,6 +153,7 @@ export const StorageManager = { USER_LANGUAGE: 'en', ASK_BUTTON: false, ASK_BUTTON_BLOCK_PAGE: [], + SHORTCUT_DISABLED_PAGES: [], }; } },