Skip to content

Commit a42f357

Browse files
committed
heuristics recovery of editor
1 parent 9265e64 commit a42f357

File tree

13 files changed

+487
-351
lines changed

13 files changed

+487
-351
lines changed

.gitattributes

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Normalize text files to LF in the repo and on checkout
2-
* text=auto eol=lf
2+
* text=auto eol=lf
33
# Keep CRLF for Windows batch files (avoid cmd.exe edge cases)
44
*.bat text eol=crlf
55
*.cmd text eol=crlf

code-notes.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ Current architectural reference for the OneClickPrompts Chrome extension (Manife
3939
- Manages keyboard shortcuts (Alt+1‒0) with idempotent listener registration and respects the `enableShortcuts` flag.
4040
- Cleans up resiliency timers and observers before re-running `publicStaticVoidMain()` to prevent duplicates.
4141

42+
### 2.4 Selector Auto-Detection System
43+
- **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.
48+
4249
## 3. UI Surfaces
4350

4451
### 3.1 Inline Toolbar & Buttons

manifest.json

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,12 @@
33
"name": "OneClickPrompts",
44
"description": "One Click Prompts for AI chat interfaces",
55
"version": "0.0.4.8",
6-
76
"icons": {
87
"16": "icon16.png",
98
"32": "icon32.png",
109
"48": "icon48.png",
1110
"128": "icon128.png"
1211
},
13-
1412
"action": {
1513
"default_popup": "popup.html",
1614
"default_icon": {
@@ -22,14 +20,14 @@
2220
"default_title": "OneClickPrompts: Open User Interface"
2321
},
2422
"options_page": "popup.html",
25-
26-
"permissions": ["storage", "contextMenus"],
27-
23+
"permissions": [
24+
"storage",
25+
"contextMenus"
26+
],
2827
"background": {
2928
"service_worker": "config.js",
3029
"type": "module"
3130
},
32-
3331
"content_scripts": [
3432
{
3533
"matches": [
@@ -44,11 +42,17 @@
4442
"https://www.perplexity.ai/*",
4543
"https://perplexity.ai/*"
4644
],
47-
"css": ["floating-panel-files/floating-panel.css", "common-ui-elements/ocp_toast.css"],
45+
"css": [
46+
"floating-panel-files/floating-panel.css",
47+
"common-ui-elements/ocp_toast.css"
48+
],
4849
"js": [
4950
"log.js",
50-
"event-handlers.js",
51-
"utils.js",
51+
"event-handlers.js",
52+
"utils.js",
53+
"modules/selector-auto-detector/base-heuristics.js",
54+
"modules/selector-auto-detector/index.js",
55+
"modules/selector-auto-detector/selector-guard.js",
5256
"/modules/token-models/base.js",
5357
"/modules/token-models/registry.js",
5458
"/modules/token-models/model-simple.js",
@@ -88,8 +92,12 @@
8892
],
8993
"web_accessible_resources": [
9094
{
91-
"resources": ["floating-panel-files/floating-panel.html"],
92-
"matches": ["<all_urls>"]
95+
"resources": [
96+
"floating-panel-files/floating-panel.html"
97+
],
98+
"matches": [
99+
"<all_urls>"
100+
]
93101
}
94102
]
95-
}
103+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* File: modules/selector-auto-detector/base-heuristics.js
3+
* Version: 1.0
4+
*
5+
* Description:
6+
* This module provides the base heuristics for detecting UI elements when standard selectors fail.
7+
* Currently a stub that logs attempts and returns null.
8+
* Future versions will implement real DOM analysis strategies.
9+
*/
10+
11+
'use strict';
12+
13+
window.OneClickPromptsSelectorAutoDetectorBase = {
14+
/**
15+
* Attempts to find the chat editor using heuristics.
16+
* @returns {HTMLElement|null} The found editor element or null.
17+
*/
18+
detectEditor: async function () {
19+
logConCgp('[SelectorAutoDetector] Running editor heuristics...');
20+
21+
// 1. Find all potential candidates
22+
const candidates = [
23+
...document.querySelectorAll('textarea'),
24+
...document.querySelectorAll('div[contenteditable="true"]')
25+
];
26+
27+
logConCgp(`[SelectorAutoDetector] Found ${candidates.length} initial candidates.`);
28+
29+
// 2. Filter for visibility and size
30+
const visibleCandidates = candidates.filter(el => {
31+
const rect = el.getBoundingClientRect();
32+
const style = window.getComputedStyle(el);
33+
34+
const isVisible = style.display !== 'none' &&
35+
style.visibility !== 'hidden' &&
36+
style.opacity !== '0' &&
37+
rect.width > 10 &&
38+
rect.height > 10; // Arbitrary small size to filter out tiny hidden inputs
39+
40+
return isVisible;
41+
});
42+
43+
logConCgp(`[SelectorAutoDetector] ${visibleCandidates.length} candidates after visibility filter.`);
44+
45+
if (visibleCandidates.length === 0) {
46+
return null;
47+
}
48+
49+
// 3. Sort by vertical position (lowest on page first)
50+
// We use rect.top to determine vertical position. Higher value = lower on page.
51+
visibleCandidates.sort((a, b) => {
52+
const rectA = a.getBoundingClientRect();
53+
const rectB = b.getBoundingClientRect();
54+
return rectB.top - rectA.top;
55+
});
56+
57+
const bestMatch = visibleCandidates[0];
58+
logConCgp('[SelectorAutoDetector] Best match found:', bestMatch);
59+
60+
return bestMatch;
61+
},
62+
63+
/**
64+
* Attempts to find the send button using heuristics.
65+
* @returns {HTMLElement|null} The found send button element or null.
66+
*/
67+
detectSendButton: async function () {
68+
logConCgp('[SelectorAutoDetector] Running send button heuristics...');
69+
// Placeholder for future logic:
70+
// 1. Find all buttons
71+
// 2. Score based on icon (SVG), aria-label ('Send', 'Submit'), position relative to editor
72+
return null;
73+
}
74+
};
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* File: modules/selector-auto-detector/index.js
3+
* Version: 1.0
4+
*
5+
* Description:
6+
* The "Brain" of the selector auto-detection system.
7+
* Manages failure tracking, coordinates recovery attempts, and handles user notifications.
8+
*/
9+
10+
'use strict';
11+
12+
window.OneClickPromptsSelectorAutoDetector = {
13+
state: {
14+
editor: {
15+
failures: 0,
16+
lastFailure: 0,
17+
recovering: false
18+
},
19+
sendButton: {
20+
failures: 0,
21+
lastFailure: 0,
22+
recovering: false
23+
}
24+
},
25+
26+
config: {
27+
failureThreshold: 1, // Number of failures before triggering recovery (can be >1 to debounce)
28+
cooldownMs: 2000, // Time to wait before re-alerting or re-trying
29+
},
30+
31+
/**
32+
* Reports a failure to find a specific element type.
33+
* @param {string} type - 'editor' or 'sendButton'
34+
* @param {Object} context - Additional context (e.g., selectors tried)
35+
*/
36+
reportFailure: async function (type, context = {}) {
37+
const now = Date.now();
38+
const s = this.state[type];
39+
40+
if (!s) {
41+
logConCgp(`[SelectorAutoDetector] Unknown type reported: ${type}`);
42+
return null;
43+
}
44+
45+
// Debounce/Cooldown check
46+
if (s.recovering || (now - s.lastFailure < this.config.cooldownMs)) {
47+
return null;
48+
}
49+
50+
s.failures++;
51+
s.lastFailure = now;
52+
53+
logConCgp(`[SelectorAutoDetector] ${type} failure reported. Count: ${s.failures}`, context);
54+
55+
if (s.failures >= this.config.failureThreshold) {
56+
return await this.triggerRecovery(type);
57+
}
58+
return null;
59+
},
60+
61+
/**
62+
* Reports that an element was successfully found.
63+
* Resets failure counters.
64+
* @param {string} type - 'editor' or 'sendButton'
65+
*/
66+
reportRecovery: function (type) {
67+
const s = this.state[type];
68+
if (s && s.failures > 0) {
69+
logConCgp(`[SelectorAutoDetector] ${type} recovered. Resetting state.`);
70+
s.failures = 0;
71+
s.recovering = false;
72+
}
73+
},
74+
75+
/**
76+
* Initiates the recovery process.
77+
* @param {string} type - 'editor' or 'sendButton'
78+
* @returns {Promise<HTMLElement|null>}
79+
*/
80+
triggerRecovery: async function (type) {
81+
const s = this.state[type];
82+
s.recovering = true;
83+
84+
// Readable name for the type
85+
const typeName = type === 'editor' ? 'Text input area' : 'send button';
86+
87+
// Notify user
88+
if (window.showToast) {
89+
window.showToast(`OneClickPrompts: ${typeName} not found. Trying to find it...`, 'info');
90+
} else {
91+
console.warn(`OneClickPrompts: ${typeName} not found. Analyzing page structure...`);
92+
}
93+
94+
// Run Heuristics
95+
let result = null;
96+
if (type === 'editor') {
97+
result = await window.OneClickPromptsSelectorAutoDetectorBase.detectEditor();
98+
} else if (type === 'sendButton') {
99+
result = await window.OneClickPromptsSelectorAutoDetectorBase.detectSendButton();
100+
}
101+
102+
if (result) {
103+
logConCgp(`[SelectorAutoDetector] Heuristics found new ${type}!`, result);
104+
// TODO: Save new selector to storage
105+
// Removed "Found!" toast to avoid false positives if the element turns out to be invalid.
106+
s.failures = 0;
107+
} else {
108+
logConCgp(`[SelectorAutoDetector] Heuristics failed to find ${type}.`);
109+
if (window.showToast) window.showToast(`OneClickPrompts: Could not find ${typeName}. Please report this issue.`, 'error');
110+
}
111+
112+
s.recovering = false;
113+
return result;
114+
}
115+
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* File: modules/selector-auto-detector/selector-guard.js
3+
* Version: 1.0
4+
*
5+
* Description:
6+
* The "Guard" adapter that replaces direct DOM queries.
7+
* It acts as a proxy, trying standard selectors first, then reporting success/failure
8+
* to the AutoDetector.
9+
*/
10+
11+
'use strict';
12+
13+
window.OneClickPromptsSelectorGuard = {
14+
/**
15+
* Tries to find the editor element using known selectors.
16+
* @returns {Promise<HTMLElement|null>}
17+
*/
18+
findEditor: async function () {
19+
const selectors = window.InjectionTargetsOnWebsite?.selectors?.editors || [];
20+
21+
// 1. Try standard selectors
22+
let element = this._querySelectors(selectors);
23+
24+
// 2. Handle Result
25+
if (element) {
26+
window.OneClickPromptsSelectorAutoDetector.reportRecovery('editor');
27+
return element;
28+
} else {
29+
// Try to recover
30+
return await window.OneClickPromptsSelectorAutoDetector.reportFailure('editor', { selectors });
31+
}
32+
},
33+
34+
/**
35+
* Tries to find the send button element using known selectors.
36+
* @returns {Promise<HTMLElement|null>}
37+
*/
38+
findSendButton: async function () {
39+
const selectors = window.InjectionTargetsOnWebsite?.selectors?.sendButtons || [];
40+
41+
// 1. Try standard selectors
42+
let element = this._querySelectors(selectors);
43+
44+
// 2. Handle Result
45+
if (element) {
46+
window.OneClickPromptsSelectorAutoDetector.reportRecovery('sendButton');
47+
return element;
48+
} else {
49+
// Try to recover
50+
return await window.OneClickPromptsSelectorAutoDetector.reportFailure('sendButton', { selectors });
51+
}
52+
},
53+
54+
/**
55+
* Helper to iterate selectors and find the first matching visible element.
56+
* @param {string[]} selectors
57+
* @returns {HTMLElement|null}
58+
*/
59+
_querySelectors: function (selectors) {
60+
if (!selectors || selectors.length === 0) return null;
61+
62+
// Try to find a visible element first
63+
// Some sites have multiple hidden textareas, we usually want the visible one
64+
const candidates = selectors
65+
.map(s => document.querySelectorAll(s))
66+
.flatMap(nodeList => Array.from(nodeList));
67+
68+
// Filter for existence and basic visibility (offsetParent is a quick check for 'display: none')
69+
const visibleCandidate = candidates.find(el => el && el.offsetParent !== null);
70+
71+
if (visibleCandidate) return visibleCandidate;
72+
73+
// Fallback to just existence if no visible candidate found (rare but possible)
74+
return candidates.find(el => el) || null;
75+
}
76+
};

0 commit comments

Comments
 (0)