Skip to content

Commit 8a15a72

Browse files
committed
add anthropic support
Signed-off-by: Bohaska <73286691+Bohaska@users.noreply.github.com>
1 parent 4b8e13d commit 8a15a72

File tree

9 files changed

+464
-140
lines changed

9 files changed

+464
-140
lines changed

chrome-extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"type-check": "tsc --noEmit"
2020
},
2121
"dependencies": {
22+
"@anthropic-ai/sdk": "^0.55.0",
2223
"@extension/env": "workspace:*",
2324
"@extension/shared": "workspace:*",
2425
"@extension/storage": "workspace:*",

chrome-extension/src/background/index.ts

Lines changed: 179 additions & 134 deletions
Large diffs are not rendered by default.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { createStorage, StorageEnum } from '../base/index.js';
2+
import type { AnthropicStorageType, AnthropicModelState } from '../types.js';
3+
4+
const storage = createStorage<AnthropicModelState>(
5+
'anthropic-storage-key',
6+
{
7+
apiKey: '',
8+
model: 'claude-3-5-haiku-latest',
9+
},
10+
{
11+
storageEnum: StorageEnum.Local,
12+
liveUpdate: true,
13+
},
14+
);
15+
16+
export const anthropicStorage: AnthropicStorageType = {
17+
...storage,
18+
setApiKey: async (apiKey: string) => {
19+
await storage.set(currentState => ({
20+
...currentState,
21+
apiKey,
22+
}));
23+
},
24+
setModel: async (model: string) => {
25+
await storage.set(currentState => ({
26+
...currentState,
27+
model,
28+
}));
29+
},
30+
};

packages/storage/lib/impl/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './example-theme-storage.js';
22
export * from './ai-model-storage.js';
33
export * from './openai-storage.js';
44
export * from './provider-storage.js';
5+
export * from './anthropic-storage.js';

packages/storage/lib/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,24 @@ export type OpenAiModelState = {
1616
model: string;
1717
};
1818

19+
export type AnthropicModelState = {
20+
apiKey: string;
21+
model: string;
22+
};
23+
1924
export type ProviderState = {
20-
provider: 'gemini' | 'openai';
25+
provider: 'gemini' | 'openai' | 'anthropic';
2126
};
2227

2328
export type AiModelStorageType = BaseStorageType<AiModelState>;
2429
export type OpenAiStorageType = BaseStorageType<OpenAiModelState> & {
2530
setApiKey: (apiKey: string) => Promise<void>;
2631
setModel: (model: string) => Promise<void>;
2732
};
33+
export type AnthropicStorageType = BaseStorageType<AnthropicModelState> & {
34+
setApiKey: (apiKey: string) => Promise<void>;
35+
setModel: (model: string) => Promise<void>;
36+
};
2837

2938
export type ProviderStorageType = BaseStorageType<ProviderState> & {
3039
setProvider: (provider: 'gemini' | 'openai') => Promise<void>;

pages/content/src/index.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
console.log('AI Autofill Pro Content Script: Initializing...');
12
import 'webextension-polyfill';
2-
import { checkRadioOrCheckbox, extractFormData, fillTextInput, selectDropdownOption } from './utils';
3+
import { checkRadioOrCheckbox, extractPageContext, fillTextInput, selectDropdownOption } from './utils';
34

45
// Function to execute actions received from the background script
56
async function executeActions(actions: any[]) {
7+
console.log('Executing actions:', actions);
68
const actionHandlers: { [key: string]: (element: HTMLElement, args: any) => void } = {
79
fill_text_input: (element, args) => fillTextInput(element as HTMLInputElement | HTMLTextAreaElement, args.value),
810
select_dropdown_option: (element, args) => selectDropdownOption(element as HTMLSelectElement, args.value),
@@ -12,12 +14,14 @@ async function executeActions(actions: any[]) {
1214
for (const action of actions) {
1315
const { tool_name, args } = action;
1416
const selector = args.selector;
17+
console.log(`Processing action: ${tool_name} for selector: ${selector} with args:`, args);
1518

1619
let element: HTMLElement | null = null;
1720
try {
1821
// Use XPath to find the element
1922
const result = document.evaluate(selector, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
2023
element = result.singleNodeValue as HTMLElement | null;
24+
console.log(`Element found for selector ${selector}:`, element);
2125
} catch (e) {
2226
console.warn(`Error evaluating XPath '${selector}':`, e);
2327
continue;
@@ -31,6 +35,7 @@ async function executeActions(actions: any[]) {
3135
try {
3236
const handler = actionHandlers[tool_name];
3337
if (handler) {
38+
console.log(`Calling handler for ${tool_name} on element:`, element);
3439
handler(element, args);
3540
} else {
3641
console.warn(`Unknown tool_name: ${tool_name}`);
@@ -54,10 +59,11 @@ async function executeActions(actions: any[]) {
5459
// Listen for messages from the background script
5560
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
5661
if (message.type === 'EXTRACT_FORM_DATA') {
57-
const formData = extractFormData();
58-
chrome.runtime.sendMessage({ type: 'FORM_DATA_EXTRACTED', payload: formData });
62+
const pageContext = extractPageContext();
63+
chrome.runtime.sendMessage({ type: 'FORM_DATA_EXTRACTED', payload: pageContext });
5964
return true; // Indicates async response
6065
} else if (message.type === 'EXECUTE_ACTIONS') {
66+
console.log('Content Script: Received EXECUTE_ACTIONS message.');
6167
executeActions(message.payload);
6268
return true; // Indicates async response
6369
}

pages/content/src/utils.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
import 'webextension-polyfill';
22

3+
// Helper to check if an element is visible
4+
export function isElementVisible(el: Element): boolean {
5+
if (!(el instanceof HTMLElement)) return false;
6+
const style = window.getComputedStyle(el);
7+
return (
8+
style.display !== 'none' &&
9+
style.visibility !== 'hidden' &&
10+
style.opacity !== '0' &&
11+
el.offsetWidth > 0 &&
12+
el.offsetHeight > 0
13+
);
14+
}
15+
16+
// Helper to check if an element contains only text nodes as children
17+
export function hasOnlyTextNodeChildren(element: HTMLElement): boolean {
18+
if (!element.hasChildNodes()) {
19+
return true; // No children, so effectively only text nodes (or none)
20+
}
21+
for (let i = 0; i < element.childNodes.length; i++) {
22+
const child = element.childNodes[i];
23+
if (child.nodeType !== Node.TEXT_NODE) {
24+
return false; // Found a non-text node child
25+
}
26+
}
27+
return true; // All children are text nodes
28+
}
29+
330
// Function to get a robust XPath for an element
431
export function getElementXPath(element: Element): string {
532
if (element.id !== '') {
@@ -83,18 +110,146 @@ export function extractFormData() {
83110

84111
// Helper functions for specific actions
85112
export function fillTextInput(element: HTMLInputElement | HTMLTextAreaElement, value: string) {
113+
console.log(`Filling text input with selector: ${getElementXPath(element)} with value: ${value}`);
86114
element.value = value;
87115
element.dispatchEvent(new Event('input', { bubbles: true }));
88116
element.dispatchEvent(new Event('change', { bubbles: true }));
89117
}
90118

91119
export function selectDropdownOption(element: HTMLSelectElement, value: string) {
120+
console.log(`Selecting dropdown option with selector: ${getElementXPath(element)} with value: ${value}`);
92121
element.value = value;
93122
element.dispatchEvent(new Event('change', { bubbles: true }));
94123
}
95124

96125
export function checkRadioOrCheckbox(element: HTMLInputElement, checked: boolean) {
126+
console.log(`Checking radio/checkbox with selector: ${getElementXPath(element)} with checked: ${checked}`);
97127
element.checked = checked;
98128
element.dispatchEvent(new Event('click', { bubbles: true })); // Click often triggers more reliably for checkboxes/radios
99129
element.dispatchEvent(new Event('change', { bubbles: true }));
100130
}
131+
132+
// Function to extract all relevant page content (form elements and surrounding text)
133+
export function extractPageContext() {
134+
const pageContextItems: any[] = [];
135+
let domOrderCounter = 0;
136+
137+
// Whitelist of tags from which to extract general text content
138+
const TEXT_EXTRACTION_TAGS_WHITELIST = new Set([
139+
'LABEL', 'DIV', 'P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'SPAN', 'LI', 'TD', 'TH', 'A', 'BUTTON'
140+
]);
141+
142+
const traverse = (node: Node) => {
143+
if (!node) return;
144+
145+
// Skip script, style, noscript, and comment nodes entirely
146+
if (node.nodeType === Node.ELEMENT_NODE) {
147+
const el = node as HTMLElement;
148+
const tagName = el.tagName.toUpperCase();
149+
if (['SCRIPT', 'STYLE', 'NOSCRIPT'].includes(tagName)) {
150+
return; // Skip this node and its children
151+
}
152+
} else if (node.nodeType === Node.COMMENT_NODE) {
153+
return; // Skip comment nodes
154+
}
155+
156+
const currentDomOrder = domOrderCounter++;
157+
158+
if (node.nodeType === Node.ELEMENT_NODE) {
159+
const el = node as HTMLElement;
160+
161+
if (isElementVisible(el)) { // Keep visibility check
162+
// Check if it's a form element
163+
if (el.matches('input, textarea, select')) {
164+
const data: any = {
165+
tagName: el.tagName.toLowerCase(),
166+
type: (el as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement).type || el.tagName.toLowerCase(),
167+
id: el.id,
168+
name: (el as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement).name,
169+
placeholder: ('placeholder' in el) ? (el as HTMLInputElement | HTMLTextAreaElement).placeholder : undefined,
170+
value: (el as HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement).value,
171+
selector: getElementXPath(el),
172+
domOrder: currentDomOrder,
173+
};
174+
175+
// Get associated label text
176+
let labelText = '';
177+
const inputEl = el as HTMLInputElement;
178+
if (inputEl.labels && inputEl.labels.length > 0) {
179+
const firstLabel = inputEl.labels[0];
180+
if (firstLabel) {
181+
labelText = firstLabel.textContent || '';
182+
}
183+
} else {
184+
let current = el.previousElementSibling;
185+
while (current) {
186+
if (current.tagName.toLowerCase() === 'label') {
187+
labelText = current.textContent || '';
188+
break;
189+
}
190+
current = current.previousElementSibling;
191+
}
192+
const parent = el.parentElement;
193+
if (!labelText && parent?.tagName.toLowerCase() === 'label') {
194+
labelText = parent.textContent || '';
195+
}
196+
}
197+
data.labelText = labelText.trim();
198+
199+
// Get aria-label or aria-labelledby
200+
data.ariaLabel = el.getAttribute('aria-label');
201+
data.ariaLabelledBy = el.getAttribute('aria-labelledby');
202+
203+
if (el.tagName.toLowerCase() === 'select') {
204+
data.options = Array.from((el as HTMLSelectElement).options).map(opt => ({
205+
text: opt.textContent,
206+
value: opt.value,
207+
}));
208+
} else if (el.type === 'radio' || el.type === 'checkbox') {
209+
data.checked = (el as HTMLInputElement).checked;
210+
}
211+
pageContextItems.push({ type: 'formField', domOrder: currentDomOrder, selector: data.selector, formData: data });
212+
} else {
213+
// Extract text content from non-form elements if visible and in whitelist
214+
const text = el.textContent?.trim();
215+
const tagName = el.tagName.toUpperCase();
216+
// Heuristic to avoid capturing text already covered by form field labels or redundant text
217+
const isFormRelated = el.closest('label, input, textarea, select');
218+
// Check if tag is in whitelist, text is meaningful, not form-related, and element only contains text nodes
219+
if (text && text.length > 1 && TEXT_EXTRACTION_TAGS_WHITELIST.has(tagName) && !isFormRelated && hasOnlyTextNodeChildren(el)) {
220+
pageContextItems.push({
221+
type: 'text',
222+
domOrder: currentDomOrder,
223+
selector: getElementXPath(el),
224+
text: text,
225+
});
226+
}
227+
}
228+
}
229+
} else if (node.nodeType === Node.TEXT_NODE) {
230+
const text = node.textContent?.trim();
231+
const parentElement = node.parentElement;
232+
// Ensure parent is visible, its tag is in whitelist, text is meaningful, not form-related, and parent only contains text nodes
233+
if (text && text.length > 1 && parentElement && isElementVisible(parentElement)) {
234+
const parentTagName = parentElement.tagName.toUpperCase();
235+
if (TEXT_EXTRACTION_TAGS_WHITELIST.has(parentTagName) &&
236+
!parentElement.matches('input, textarea, select, label, option') && // Avoid text within form elements/labels, and option elements
237+
hasOnlyTextNodeChildren(parentElement)) { // Add this condition
238+
pageContextItems.push({
239+
type: 'text',
240+
domOrder: currentDomOrder,
241+
selector: getElementXPath(parentElement), // Use parent's XPath for context
242+
text: text,
243+
});
244+
}
245+
}
246+
}
247+
248+
// Recursively traverse children
249+
node.childNodes.forEach(traverse);
250+
};
251+
252+
traverse(document.body); // Start traversal from the body
253+
254+
return pageContextItems;
255+
}

0 commit comments

Comments
 (0)