Skip to content

Commit

Permalink
feat(shortcuts): add support for disabling keyboard shortcuts on spec…
Browse files Browse the repository at this point in the history
…ific 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
  • Loading branch information
crazygo committed Jan 26, 2025
1 parent 0441c30 commit f37d83e
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 23 deletions.
16 changes: 15 additions & 1 deletion src/assets/conf/preferences.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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/*",
]
23 changes: 22 additions & 1 deletion src/components/ask-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLDivElement> {
code: string;
Expand Down Expand Up @@ -397,6 +398,17 @@ function AskPanel(props: AskPanelProps) {
setUserInput('');
};

const blockConfigRef = useRef<BlockConfig>(null);

// 初始化配置
useEffect(() => {
const initBlockConfig = async () => {
blockConfigRef.current = BlockConfig.getInstance();
await blockConfigRef.current.initialize();
};
initBlockConfig();
}, []);

// myObject.test('你是谁');
// console.log('history = ' + JSON.stringify(history));
return (
Expand Down Expand Up @@ -427,7 +439,16 @@ function AskPanel(props: AskPanelProps) {
{...rest}>
<div className="font-medium rounded-lg bg-transparent bg-gradient-to-r from-white via-white to-white/60 mb-2 text-base flex justify-between">
<span>
Askman <KeyBinding text="⌘ I"></KeyBinding>{' '}
Askman{' '}
{blockConfigRef.current?.isShortcutDisabled(window.location.href) ? (
<KeyBinding text="⌘ I" className="relative group opacity-50 cursor-not-allowed">
<div className="absolute left-1/2 -translate-x-1/2 -top-8 px-2 py-1 bg-gray-900 text-white text-xs rounded opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none whitespace-nowrap z-20">
This shortcut will not work on this site to avoid conflicts with website's functionality.
</div>
</KeyBinding>
) : (
<KeyBinding text="⌘ I" className="cursor-pointer" />
)}{' '}
<KeyBinding
text="Setting"
className="hover:bg-gray-300 cursor-pointer"
Expand Down
30 changes: 30 additions & 0 deletions src/components/base/Notification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useState, useEffect } from 'react';

interface NotificationProps {
message: string;
duration?: number;
onClose?: () => 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 (
<div
className="fixed top-4 right-4 bg-black text-white px-4 py-2 rounded-lg shadow-lg opacity-90 transition-opacity duration-200"
style={{ zIndex: 2147483647 }}>
{message}
</div>
);
}
4 changes: 3 additions & 1 deletion src/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<b
className={`bg-gray-100 rounded-md py-1 px-2 font-medium text-sm text-black text-opacity-50 ${className}`}
onClick={onClick}>
{text}
{children}
</b>
);
}
Expand Down
23 changes: 10 additions & 13 deletions src/pages/background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TabMessage>(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<TabMessage>(tab.id, {
cmd: CommandType.ChatPopupDisplay,
pageUrl: tab.url,
fromShortcut: true, // 标记为快捷键触发
});
} catch (error) {
console.error('Failed to handle command:', error);
}
}
});
break;
Expand Down
64 changes: 57 additions & 7 deletions src/pages/content/ui/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>(false);
const [askPanelVisible, setAskPanelVisible] = useState<boolean>(false);
const [showDoubleClickHint, setShowDoubleClickHint] = useState<boolean>(false);
const targetDom = useRef<HTMLElement>(null);
const [pageActionButton, setPageActionButton] = useState<HTMLDivElement>(null);
const [preferences, setPreferences] = useState({ ASK_BUTTON: false, ASK_BUTTON_BLOCK_PAGE: [] });
const [askPanelQuotes, setAskPanelQuotes] = useState<Promise<QuoteContext>[]>([]);
const [parentRect, setParentRect] = useState<DOMRect>();
const blockConfig = useRef<BlockConfig>(null);

// 初始化配置
useEffect(() => {
const initBlockConfig = async () => {
blockConfig.current = BlockConfig.getInstance();
await blockConfig.current.initialize();
};
initBlockConfig();
}, []);

// 加载用户偏好设置
useEffect(() => {
Expand All @@ -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)) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
};

Expand Down Expand Up @@ -180,6 +222,14 @@ export default function App() {

return (
<>
{!askPanelVisible && showDoubleClickHint && (
<Notification
message="Press Command+I/Ctrl+I again within 1s to open AskMan"
onClose={() => {
setShowDoubleClickHint(false);
}}
/>
)}
{PageStackoverflowAgent.isSupport(window.location.href) &&
pageActionButton &&
createPortal(
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface TabMessage {
cmd: CommandType;
selectionText?: string; // tab 内选择的文字
pageUrl?: string; // tab 对应的 url
fromShortcut?: boolean; // 是否由快捷键触发
linkText?: string; // 被点击的链接文本
linkUrl?: string; // 被点击的链接URL
}
Expand Down
64 changes: 64 additions & 0 deletions src/utils/BlockConfig.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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<void> {
this.patterns = patterns;
const preferences = await StorageManager.getUserPreferences();
await StorageManager.saveUserPreferences({
...preferences,
SHORTCUT_DISABLED_PAGES: patterns,
});
}

public getPatterns(): string[] {
return this.patterns;
}
}
6 changes: 6 additions & 0 deletions src/utils/StorageManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);

Expand All @@ -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;
Expand All @@ -148,6 +153,7 @@ export const StorageManager = {
USER_LANGUAGE: 'en',
ASK_BUTTON: false,
ASK_BUTTON_BLOCK_PAGE: [],
SHORTCUT_DISABLED_PAGES: [],
};
}
},
Expand Down

0 comments on commit f37d83e

Please sign in to comment.