Skip to content

Commit 26d3eae

Browse files
committed
Enhance heuristics for editor and send button detection; add DeepSeek-specific logic and improve element usability checks
1 parent 4d120f9 commit 26d3eae

File tree

10 files changed

+335
-37
lines changed

10 files changed

+335
-37
lines changed

buttons-injection.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,16 @@ function doCustomModificationsExist() {
3838
}
3939
const el = document.getElementById(containerId);
4040
// The container must exist AND have child elements (buttons/toggles) inside it.
41-
return !!(el && el.children.length > 0);
41+
if (!el || el.children.length === 0) {
42+
return false;
43+
}
44+
if (window.MaxExtensionUtils && typeof window.MaxExtensionUtils.isElementUsableForInjection === 'function') {
45+
if (!window.MaxExtensionUtils.isElementUsableForInjection(el)) {
46+
logConCgp('[button-injection] Container exists but is hidden/inert; will reinject.');
47+
return false;
48+
}
49+
}
50+
return true;
4251
}
4352

4453
/**

code-notes.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,11 @@ Current architectural reference for the OneClickPrompts Chrome extension (Manife
4141

4242
### 2.4 Selector Auto-Detection System
4343
- **Purpose**: Robustly handle DOM structure changes on AI websites by decoupling site scripts from direct DOM queries and implementing self-healing capabilities.
44-
- **`modules/selector-auto-detector/index.js`** ("The Brain"): Manages detection state, tracks failure counts/cooldowns, orchestrates the recovery workflow (Failure -> Wait -> Heuristics -> Notify), and persists new selectors.
45-
- **`modules/selector-auto-detector/selector-guard.js`** ("The Guard"): Adapter that replaces direct `document.querySelector` calls. Wraps lookups in a Promise, reporting success/failure to the Brain.
46-
- **`modules/selector-auto-detector/base-heuristics.js`**: Contains logic to "guess" new selectors when known ones fail (currently a stub).
47-
- **Integration**: Site-specific scripts (e.g., `buttons-clicking-grok.js`) use `SelectorGuard.findEditor()` and `findSendButton()` instead of direct queries.
44+
- **`modules/selector-auto-detector/index.js`** ("The Brain"): Manages detection state, tracks failure counts/cooldowns, orchestrates the recovery workflow (Failure -> Wait -> Heuristics -> Notify/Toast), and resets on success.
45+
- **`modules/selector-auto-detector/selector-guard.js`** ("The Guard"): Adapter that replaces direct `document.querySelector` calls. Tries configured selectors, skips disabled/custom buttons, reports success/failure to the Brain.
46+
- **`modules/selector-auto-detector/base-heuristics.js`**: Generic heuristics (textarea + any `[contenteditable]`, shadow-root walk; buttons/role=button scoring). Also hosts `OneClickPromptsSiteHeuristics` registry and resolver.
47+
- **Site heuristics modules**: e.g., `modules/selector-auto-detector/heuristics-deepseek.js` registers DeepSeek-specific editor/button scoring (ignores OCP UI). If present, the Brain picks the site module by `InjectionTargetsOnWebsite.activeSite`; otherwise it falls back to the base heuristics.
48+
- **Integration**: Site scripts call `SelectorGuard.findEditor()/findSendButton()`; on repeated failures the Brain runs heuristics, shows toasts on success/error, and returns the guessed element to the caller.
4849

4950
## 3. UI Surfaces
5051

manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"event-handlers.js",
5252
"utils.js",
5353
"modules/selector-auto-detector/base-heuristics.js",
54+
"modules/selector-auto-detector/heuristics-deepseek.js",
5455
"modules/selector-auto-detector/index.js",
5556
"modules/selector-auto-detector/selector-guard.js",
5657
"/modules/token-models/base.js",
@@ -100,4 +101,4 @@
100101
]
101102
}
102103
]
103-
}
104+
}

modules/selector-auto-detector/base-heuristics.js

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,24 @@ window.OneClickPromptsSelectorAutoDetectorBase = {
1818
detectEditor: async function () {
1919
logConCgp('[SelectorAutoDetector] Running editor heuristics...');
2020

21-
// 1. Find all potential candidates
22-
const candidates = [
23-
...document.querySelectorAll('textarea'),
24-
...document.querySelectorAll('div[contenteditable="true"]')
21+
// 1. Find all potential candidates, including generic contenteditables and shadow roots.
22+
const collectEditables = (root) => [
23+
...root.querySelectorAll('textarea, [contenteditable="true"]')
2524
];
2625

26+
const candidates = collectEditables(document);
27+
28+
// Traverse shadow roots to catch editors nested in web components (e.g., Lexical hosts)
29+
const walkShadowRoots = (root) => {
30+
root.querySelectorAll('*').forEach(el => {
31+
if (el.shadowRoot) {
32+
candidates.push(...collectEditables(el.shadowRoot));
33+
walkShadowRoots(el.shadowRoot);
34+
}
35+
});
36+
};
37+
walkShadowRoots(document);
38+
2739
logConCgp(`[SelectorAutoDetector] Found ${candidates.length} initial candidates.`);
2840

2941
// 2. Filter for visibility and size
@@ -181,3 +193,25 @@ window.OneClickPromptsSelectorAutoDetectorBase = {
181193
return null;
182194
}
183195
};
196+
197+
// Registry enabling site-aware heuristics modules to plug in their own detectors.
198+
window.OneClickPromptsSiteHeuristics = window.OneClickPromptsSiteHeuristics || {
199+
registry: {},
200+
/**
201+
* Register heuristics for a specific site name (e.g., "DeepSeek").
202+
* @param {string} site
203+
* @param {{detectEditor: function, detectSendButton: function}} impl
204+
*/
205+
register(site, impl) {
206+
if (!site || !impl) return;
207+
this.registry[site] = impl;
208+
},
209+
/**
210+
* Resolve heuristics by site, falling back to the base heuristics.
211+
* @param {string} site
212+
* @returns {object}
213+
*/
214+
resolve(site) {
215+
return this.registry[site] || window.OneClickPromptsSelectorAutoDetectorBase;
216+
}
217+
};
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* DeepSeek-specific heuristics for editor and send button discovery.
3+
* Registered with the site heuristics registry so the Brain can pick it up when DeepSeek is active.
4+
*/
5+
6+
'use strict';
7+
8+
(function () {
9+
const isVisible = (el) => {
10+
if (!el) return false;
11+
const rect = el.getBoundingClientRect();
12+
const style = window.getComputedStyle(el);
13+
return rect.width > 10 &&
14+
rect.height > 10 &&
15+
style.display !== 'none' &&
16+
style.visibility !== 'hidden' &&
17+
style.opacity !== '0';
18+
};
19+
20+
const unique = (list) => {
21+
const seen = new Set();
22+
return list.filter((el) => {
23+
if (!el) return false;
24+
if (seen.has(el)) return false;
25+
seen.add(el);
26+
return true;
27+
});
28+
};
29+
30+
const detectEditor = async () => {
31+
// Note: this selector list mirrors the defaults in utils.js so heuristics can still
32+
// guess an editor when the Guard's configured selectors fail (e.g., class churn).
33+
const selectors = [
34+
'textarea[placeholder="Message DeepSeek"]',
35+
'textarea[aria-label*="Message"]',
36+
'textarea[placeholder*="Message"]',
37+
'[class*="chat-input"] textarea',
38+
'[class*="chat-input"] [contenteditable="true"]',
39+
'textarea',
40+
'div[contenteditable="true"]',
41+
'textarea._27c9245',
42+
'textarea.ds-scroll-area'
43+
];
44+
45+
const candidates = unique(
46+
selectors.flatMap(sel => Array.from(document.querySelectorAll(sel)))
47+
).filter(isVisible);
48+
49+
if (candidates.length === 0) return null;
50+
51+
const scored = candidates.map(el => {
52+
let score = 0;
53+
const placeholder = (el.getAttribute('placeholder') || '').toLowerCase();
54+
const aria = (el.getAttribute('aria-label') || '').toLowerCase();
55+
const cls = el.className || '';
56+
57+
if (placeholder === 'message deepseek') score += 12;
58+
else if (placeholder.includes('message') || aria.includes('message')) score += 8;
59+
if (cls.includes('ds-scroll-area') || cls.includes('_27c9245')) score += 2;
60+
if (el.tagName === 'TEXTAREA') score += 3;
61+
62+
const footer = el.closest('[class*="editor"], [class*="footer"], form');
63+
if (footer) score += 2;
64+
65+
const rect = el.getBoundingClientRect();
66+
score += rect.top; // prefer lower on the page
67+
68+
return { el, score };
69+
});
70+
71+
scored.sort((a, b) => b.score - a.score);
72+
return scored[0]?.el || null;
73+
};
74+
75+
const detectSendButton = async () => {
76+
const editor = await detectEditor().catch(() => null);
77+
const editorRect = editor ? editor.getBoundingClientRect() : null;
78+
79+
const candidates = unique([
80+
...document.querySelectorAll('.ds-icon-button'),
81+
...document.querySelectorAll('button'),
82+
...document.querySelectorAll('[role="button"]'),
83+
...document.querySelectorAll('[data-testid]')
84+
]).filter(el => {
85+
if (!isVisible(el)) return false;
86+
const testId = (el.getAttribute('data-testid') || '').toLowerCase();
87+
if (testId.startsWith('custom-send-button')) return false; // ignore our own buttons
88+
const ocpContainer = el.closest('[id*="custom-buttons-container"], [data-ocp-profile-selector]');
89+
if (ocpContainer) return false; // avoid OCP UI
90+
return true;
91+
});
92+
93+
if (candidates.length === 0) return null;
94+
95+
const scored = candidates.map(el => {
96+
let score = 0;
97+
const aria = (el.getAttribute('aria-label') || '').toLowerCase();
98+
const title = (el.getAttribute('title') || '').toLowerCase();
99+
const testId = (el.getAttribute('data-testid') || '').toLowerCase();
100+
const text = (el.innerText || '').toLowerCase();
101+
const cls = (el.className || '').toLowerCase();
102+
103+
const matchesSend = (str) => str.includes('send');
104+
105+
if (matchesSend(aria) || matchesSend(title) || matchesSend(testId) || matchesSend(text)) score += 10;
106+
if (cls.includes('ds-icon-button')) score += 5;
107+
if (el.querySelector('svg')) score += 4;
108+
109+
const container = el.closest('[class*="button"], [class*="footer"], form');
110+
if (container) score += 3;
111+
112+
if (editorRect) {
113+
const rect = el.getBoundingClientRect();
114+
const verticalProximity = Math.max(0, 200 - Math.abs(rect.top - editorRect.top));
115+
const horizontalProximity = Math.max(0, 300 - Math.abs(rect.left - editorRect.right));
116+
score += verticalProximity * 0.01;
117+
score += horizontalProximity * 0.01;
118+
if (rect.top >= editorRect.top) score += 2; // below or aligned with editor
119+
}
120+
121+
if (el.disabled || el.getAttribute('aria-disabled') === 'true') {
122+
score -= 5; // avoid disabled matches that blocked recovery earlier
123+
}
124+
125+
return { el, score };
126+
});
127+
128+
scored.sort((a, b) => b.score - a.score);
129+
const best = scored[0];
130+
if (best && best.score > 0) {
131+
return best.el;
132+
}
133+
return null;
134+
};
135+
136+
if (window.OneClickPromptsSiteHeuristics?.register) {
137+
window.OneClickPromptsSiteHeuristics.register('DeepSeek', {
138+
detectEditor,
139+
detectSendButton
140+
});
141+
}
142+
})();

modules/selector-auto-detector/index.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,24 @@ window.OneClickPromptsSelectorAutoDetector = {
9191
console.warn(`OneClickPrompts: ${typeName} not found. Analyzing page structure...`);
9292
}
9393

94+
const site = window.InjectionTargetsOnWebsite?.activeSite || 'Unknown';
95+
const heuristics = window.OneClickPromptsSiteHeuristics?.resolve
96+
? window.OneClickPromptsSiteHeuristics.resolve(site)
97+
: window.OneClickPromptsSelectorAutoDetectorBase;
98+
9499
// Run Heuristics
95100
let result = null;
96101
if (type === 'editor') {
97-
result = await window.OneClickPromptsSelectorAutoDetectorBase.detectEditor();
102+
result = await heuristics.detectEditor({ site });
98103
} else if (type === 'sendButton') {
99-
result = await window.OneClickPromptsSelectorAutoDetectorBase.detectSendButton();
104+
result = await heuristics.detectSendButton({ site });
100105
}
101106

102107
if (result) {
103108
logConCgp(`[SelectorAutoDetector] Heuristics found new ${type}!`, result);
109+
if (window.showToast) {
110+
window.showToast(`OneClickPrompts: Found the ${typeName}.`, 'success');
111+
}
104112
// TODO: Save new selector to storage
105113
// Removed "Found!" toast to avoid false positives if the element turns out to be invalid.
106114
s.failures = 0;

modules/selector-auto-detector/selector-guard.js

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ window.OneClickPromptsSelectorGuard = {
3939
const selectors = window.InjectionTargetsOnWebsite?.selectors?.sendButtons || [];
4040

4141
// 1. Try standard selectors
42-
let element = this._querySelectors(selectors);
42+
let element = this._querySelectors(selectors, { requireEnabled: true });
4343

4444
// 2. Handle Result
4545
if (element) {
@@ -54,9 +54,11 @@ window.OneClickPromptsSelectorGuard = {
5454
/**
5555
* Helper to iterate selectors and find the first matching visible element.
5656
* @param {string[]} selectors
57+
* @param {Object} options
5758
* @returns {HTMLElement|null}
5859
*/
59-
_querySelectors: function (selectors) {
60+
_querySelectors: function (selectors, options = {}) {
61+
const requireEnabled = options.requireEnabled || false;
6062
if (!selectors || selectors.length === 0) return null;
6163

6264
// Try to find a visible element first
@@ -66,11 +68,31 @@ window.OneClickPromptsSelectorGuard = {
6668
.flatMap(nodeList => Array.from(nodeList));
6769

6870
// Filter for existence and basic visibility (offsetParent is a quick check for 'display: none')
69-
const visibleCandidate = candidates.find(el => el && el.offsetParent !== null);
71+
const visibleCandidate = candidates.find(el => {
72+
if (!el || el.offsetParent === null) return false;
73+
if (el.getAttribute) {
74+
const testId = (el.getAttribute('data-testid') || '').toLowerCase();
75+
if (testId.startsWith('custom-send-button')) return false;
76+
}
77+
if (el.closest && el.closest('[id*="custom-buttons-container"]')) return false;
78+
if (!requireEnabled) return true;
79+
const ariaDisabled = el.getAttribute && el.getAttribute('aria-disabled');
80+
return !el.disabled && ariaDisabled !== 'true';
81+
});
7082

7183
if (visibleCandidate) return visibleCandidate;
7284

7385
// Fallback to just existence if no visible candidate found (rare but possible)
74-
return candidates.find(el => el) || null;
86+
return candidates.find(el => {
87+
if (!el) return false;
88+
if (el.getAttribute) {
89+
const testId = (el.getAttribute('data-testid') || '').toLowerCase();
90+
if (testId.startsWith('custom-send-button')) return false;
91+
}
92+
if (el.closest && el.closest('[id*="custom-buttons-container"]')) return false;
93+
if (!requireEnabled) return true;
94+
const ariaDisabled = el.getAttribute && el.getAttribute('aria-disabled');
95+
return !el.disabled && ariaDisabled !== 'true';
96+
}) || null;
7597
}
7698
};

per-website-button-clicking-mechanics/buttons-clicking-perplexity.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,10 +203,18 @@ function perplexityEditorHasContent(expectedText, editorElement) {
203203
return false;
204204
}
205205
const currentText = editorElement.innerText || editorElement.textContent || '';
206+
const normalizedCurrent = currentText.replace(/\s+/g, '').toLowerCase();
207+
206208
if (expectedText) {
207-
return currentText.includes(expectedText.trim().slice(0, 20));
209+
const normalizedExpected = expectedText.replace(/\s+/g, '').toLowerCase();
210+
const probe = normalizedExpected.slice(0, 30);
211+
if (probe && normalizedCurrent.includes(probe)) {
212+
return true;
213+
}
208214
}
209-
return currentText.trim().length > 0;
215+
216+
// Fallback: any non-whitespace content counts as ready.
217+
return normalizedCurrent.length > 0;
210218
} catch (error) {
211219
logConCgp('[Perplexity] Error while verifying editor content:', error);
212220
return true;

popup.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -519,7 +519,7 @@ <h4>Settings</h4>
519519
<span class="slider"></span>
520520
</label>
521521
<span class="switch-label"
522-
>Danger: Auto sent to all instances of chats</span
522+
>Add dangerous button ⬆️: Auto sent text you have in editor to all instances of chats</span
523523
>
524524
</div>
525525
</div>

0 commit comments

Comments
 (0)