diff --git a/README.md b/README.md index 8f0fc14..bbe5e70 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ScreenHawk -ScreenHawk is a Chrome extension that allows you to capture a screenshot of your active tab, provide context or ask a question about it, and get an analysis from OpenAI's API. The response from OpenAI is then displayed directly on the page. +ScreenHawk is a Chrome extension that allows you to capture a screenshot of your active tab, provide context or ask a question about it, and get an analysis from OpenAI's API. The response from OpenAI is then displayed directly on the page. Additionally, ScreenHawk provides AI-powered form filling assistance to help you write content for text areas and input fields. ## Features @@ -10,9 +10,27 @@ ScreenHawk is a Chrome extension that allows you to capture a screenshot of your - **OpenAI Integration:** Seamlessly sends the screenshot and your query to the OpenAI API for in-depth analysis. - **In-Page Display:** Displays the OpenAI API's response directly on the current webpage, making it easy to view and use. - **Keyboard Shortcut:** Use `Ctrl+Shift+S` (or `Cmd+Shift+S` on Mac) for quick and easy screenshot capture. +- **🆕 AI Form Assistance:** Automatically detects form fields and provides AI-powered writing assistance with smart context extraction. + +### AI Form Assistance + +ScreenHawk now automatically adds assistance buttons (✨) to text areas and large text input fields on any webpage. When you click these buttons, you can: + +- **Get AI-powered writing help:** Simply describe what you want to write in natural language +- **Context-aware assistance:** The AI understands the field's purpose from labels, placeholders, and surrounding context +- **Smart form detection:** Automatically identifies suitable form fields (text areas and substantial text inputs) +- **Seamless integration:** Works alongside the existing screenshot functionality without interference + +**How it works:** +1. Load any webpage with forms (job applications, feedback forms, contact forms, etc.) +2. Look for the ✨ assistance buttons in the top-right corner of eligible text fields +3. Click the button to open the AI Writing Assistant +4. Describe what you want to write (e.g., "Write a professional bio for a software engineer") +5. The AI generates appropriate content and fills the field with a smooth typing animation ## How to Use +### Screenshot Analysis 1. **Activate the Extension:** Click the ScreenHawk extension icon in your Chrome toolbar or use the keyboard shortcut (`Ctrl+Shift+S` or `Cmd+Shift+S`). 2. **Capture Screenshot:** The extension will automatically capture a screenshot of your current tab. 3. **Select Area:** Click and drag to select a specific area of the screenshot you want to analyze. You can press Escape to cancel the selection. @@ -20,6 +38,20 @@ ScreenHawk is a Chrome extension that allows you to capture a screenshot of your 5. **Submit for Analysis:** Click the "Submit" button. 6. **View Results:** The response from the OpenAI API will be displayed in an overlay on the current page. +### AI Form Assistance +1. **Navigate to any webpage with forms** (job applications, feedback forms, contact forms, etc.) +2. **Look for the ✨ assistance buttons** that automatically appear in the top-right corner of text areas and large text input fields +3. **Click an assistance button** to open the AI Writing Assistant dialog +4. **Describe what you want to write** in natural language (e.g., "Write a professional summary of my experience at Apple") +5. **Click Generate** to have AI create appropriate content for the field +6. **Watch as the content is filled** with a smooth typing animation + +The AI assistant is context-aware and will consider: +- Field labels and placeholders +- Form section headings +- Field names and purposes +- Your specific writing request + ## Installation (Development Build) To install ScreenHawk for development: diff --git a/package-lock.json b/package-lock.json index 374120f..b10ec1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "screenhawk", "version": "1.0.0", "license": "ISC", + "dependencies": { + "axios": "^1.11.0" + }, "devDependencies": { "@types/axios": "^0.14.0", "@types/chrome": "^0.0.246", @@ -530,16 +533,16 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "license": "MIT" }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", - "dev": true, + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -609,6 +612,19 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001640", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001640.tgz", @@ -707,7 +723,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -847,11 +863,25 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, + "license": "MIT", "engines": { "node": ">=0.4.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.818", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.818.tgz", @@ -889,12 +919,57 @@ "node": ">=4" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-module-lexer": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.5.4.tgz", "integrity": "sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==", "dev": true }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -1052,7 +1127,6 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, "funding": [ { "type": "individual", @@ -1069,13 +1143,15 @@ } }, "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -1092,7 +1168,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1106,6 +1181,43 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -1165,6 +1277,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1180,11 +1304,37 @@ "node": ">=8" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -1404,6 +1554,15 @@ "node": ">=8" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -1436,7 +1595,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -1445,7 +1603,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -1637,8 +1794,7 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/punycode": { "version": "2.3.1", diff --git a/package.json b/package.json index e6695c6..80c14fc 100644 --- a/package.json +++ b/package.json @@ -21,5 +21,8 @@ "typescript": "^5.5.3", "webpack": "^5.92.1", "webpack-cli": "^5.1.4" + }, + "dependencies": { + "axios": "^1.11.0" } } diff --git a/public/manifest.json b/public/manifest.json index cc5968e..1186255 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -31,6 +31,13 @@ "mac": "Command+Shift+S" }, "description": "Capture screenshot and open prompt dialog" + }, + "magic-wand": { + "suggested_key": { + "default": "Ctrl+Shift+Z", + "mac": "Command+Shift+Z" + }, + "description": "Activate magic wand mode to manually select form fields for AI assistance" } } } \ No newline at end of file diff --git a/public/popup.html b/public/popup.html index 24abdf8..57fd44b 100644 --- a/public/popup.html +++ b/public/popup.html @@ -3,32 +3,92 @@ - Screenshot to OpenAI + ScreenHawk -

Screenshot to OpenAI

-

Click the button below to capture the current tab and enter a prompt.

- +

ScreenHawk

+

AI-powered screenshot analysis and form assistance

+ +
+ + + +
+ +

+ Magic Wand: Manually select any text field on the page to add AI assistance. +

+ \ No newline at end of file diff --git a/src/api.ts b/src/api.ts index bdafcde..ed51170 100644 --- a/src/api.ts +++ b/src/api.ts @@ -13,11 +13,26 @@ interface OpenAIResponse { }[]; } -export async function sendToOpenAI(prompt: string, screenshot: string): Promise { +export async function sendToOpenAI(prompt: string, screenshot?: string): Promise { console.log("sendToOpenAI called with prompt:", prompt); console.log("Screenshot data:", screenshot ? "Available" : "Not available"); try { + const messageContent: any[] = [ + { + type: 'text', + text: prompt + } + ]; + + // Only add image if screenshot is provided + if (screenshot) { + messageContent.push({ + type: 'image_url', + image_url: screenshot + }); + } + const response = await axios.post( OPENAI_API_URL, { @@ -25,16 +40,7 @@ export async function sendToOpenAI(prompt: string, screenshot: string): Promise< messages: [ { role: 'user', - content: [ - { - type: 'text', - text: prompt - }, - { - type: 'image_url', - image_url: screenshot - } - ] + content: messageContent } ], max_tokens: 500 diff --git a/src/background.ts b/src/background.ts index 2283a27..5ba4e40 100644 --- a/src/background.ts +++ b/src/background.ts @@ -13,6 +13,9 @@ chrome.commands.onCommand.addListener((command) => { if (command === "capture-screenshot") { console.log("Capture screenshot command received"); captureAndSendScreenshot(); + } else if (command === "magic-wand") { + console.log("Magic wand command received"); + activateMagicWandOnActiveTab(); } }); @@ -46,6 +49,38 @@ function captureAndSendScreenshot() { }); } +function activateMagicWandOnActiveTab() { + console.log("Activating magic wand on active tab..."); + chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { + if (chrome.runtime.lastError) { + console.error("Error querying tabs:", chrome.runtime.lastError.message); + return; + } + + const activeTab = tabs[0]; + if (!activeTab || typeof activeTab.id !== 'number') { + console.error("No active tab found or tab ID is not a number"); + return; + } + + chrome.tabs.sendMessage( + activeTab.id, + { action: "activateMagicWand" }, + (response) => { + if (chrome.runtime.lastError) { + console.error("Error sending magic wand message to content script:", chrome.runtime.lastError.message); + // Attempt to inject content script if it's not already there + if (activeTab.id) { + injectContentScript(activeTab.id); + } + } else { + console.log("Magic wand message sent to content script, response:", response); + } + } + ); + }); +} + function sendMessageToContentScript(tabId: number, screenshot: string) { console.log(`Attempting to send message to content script in tab ${tabId}...`); chrome.tabs.sendMessage( @@ -95,13 +130,17 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.log("Background: Received sendToOpenAI request"); console.log("Prompt:", request.prompt); console.log("Screenshot data:", request.screenshot ? "Available" : "Not available"); + console.log("Is form assistance:", request.isFormAssistance || false); console.log("Sending to OpenAI..."); + sendToOpenAI(request.prompt, request.screenshot) .then(response => { console.log("Received response from OpenAI:", response); chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { if (tabs[0] && tabs[0].id) { - chrome.tabs.sendMessage(tabs[0].id, {action: "openAIResponse", response}, (response) => { + // Send different message type based on whether it's form assistance + const messageAction = request.isFormAssistance ? "formFillResponse" : "openAIResponse"; + chrome.tabs.sendMessage(tabs[0].id, {action: messageAction, response}, (response) => { if (chrome.runtime.lastError) { console.error("Error sending response to content script:", chrome.runtime.lastError.message); } else { diff --git a/src/content.ts b/src/content.ts index 7882fca..a26f207 100644 --- a/src/content.ts +++ b/src/content.ts @@ -3,6 +3,15 @@ let screenshot: string | null = null; let croppedScreenshot: string | null = null; +// Form assistance functionality +let formAssistanceButtons: Map = new Map(); +let currentFormField: HTMLTextAreaElement | HTMLInputElement | null = null; + +// Magic wand mode for manual field selection +let magicWandMode: boolean = false; +let highlightedFields: HTMLElement[] = []; +let magicWandOverlay: HTMLElement | null = null; + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.log("Content script received message:", request); if (request.action === "prepareCapture") { @@ -24,10 +33,27 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { console.log("Received OpenAI response:", request.response); showOpenAIResponse(request.response); sendResponse({status: "response displayed"}); + } else if (request.action === "formFillResponse") { + console.log("Received form fill response:", request.response); + fillFormField(request.response); + sendResponse({status: "form filled"}); + } else if (request.action === "activateMagicWand") { + console.log("Activating magic wand mode via message"); + activateMagicWandMode(); + sendResponse({status: "magic wand activated"}); } return true; }); +// Initialize form assistance when page loads +document.addEventListener('DOMContentLoaded', initializeFormAssistance); +// Also run immediately in case DOMContentLoaded already fired +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeFormAssistance); +} else { + initializeFormAssistance(); +} + function showOpenAIResponse(response: string) { const responseDialog = document.createElement('div'); responseDialog.style.cssText = ` @@ -270,4 +296,828 @@ function showPromptDialog() { document.getElementById('cancel')?.addEventListener('click', () => { document.body.removeChild(dialog); }); -} \ No newline at end of file +} + +// Form assistance functionality +function initializeFormAssistance() { + console.log("Initializing form assistance..."); + detectFormFields(); + + // Set up mutation observer to handle dynamically added form fields + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.type === 'childList') { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + const element = node as Element; + // Check if the added node or its descendants contain form fields + const formFields = element.querySelectorAll('textarea, input[type="text"]'); + formFields.forEach((field) => addAssistanceButton(field as HTMLTextAreaElement | HTMLInputElement)); + + // Also check if the node itself is a form field + if (isEligibleFormField(element as HTMLElement)) { + addAssistanceButton(element as HTMLTextAreaElement | HTMLInputElement); + } + } + }); + } + }); + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); +} + +function detectFormFields() { + console.log("ScreenHawk: Detecting form fields..."); + + // Find all textarea elements and text-like input fields + const textareas = document.querySelectorAll('textarea'); + // Include inputs without type (defaults to text) and explicit text inputs + const textInputs = document.querySelectorAll('input[type="text"], input:not([type]), input[type=""]'); + + console.log(`ScreenHawk: Found ${textareas.length} textareas and ${textInputs.length} text inputs`); + + textareas.forEach((textarea) => { + console.log("ScreenHawk: Adding button to textarea:", textarea); + addAssistanceButton(textarea as HTMLTextAreaElement); + }); + + // Filter text inputs to only include larger ones (exclude small fields like search boxes) + textInputs.forEach((input) => { + if (isEligibleFormField(input as HTMLElement)) { + console.log("ScreenHawk: Adding button to eligible input:", input); + addAssistanceButton(input as HTMLInputElement); + } else { + console.log("ScreenHawk: Skipping ineligible input:", input); + } + }); +} + +function isEligibleFormField(element: HTMLElement): boolean { + if (element.tagName.toLowerCase() === 'textarea') { + return true; + } + + if (element.tagName.toLowerCase() === 'input') { + const input = element as HTMLInputElement; + // Accept text inputs, inputs without type (default to text), or empty type + if (input.type !== 'text' && input.type !== '' && input.type !== undefined) return false; + + // Skip password, email, search, etc. unless specifically text + if (input.type && input.type !== 'text' && input.type !== '') return false; + + // Skip hidden fields + const style = window.getComputedStyle(input); + if (style.display === 'none' || style.visibility === 'hidden') return false; + + // Check if it's a larger text input (not a small search box or similar) + const width = parseInt(style.width) || input.offsetWidth; + const minLength = input.minLength || input.maxLength; + + console.log(`ScreenHawk: Evaluating input - width: ${width}, maxLength: ${input.maxLength}, placeholder: "${input.placeholder}", name: "${input.name}", id: "${input.id}"`); + + // Consider it eligible if: + // - Width is substantial (>150px) OR + // - Has a substantial maxLength/minLength OR + // - Size attribute suggests it's meant for longer text OR + // - Has placeholder text suggesting longer input OR + // - Context suggests it's for longer text (description, bio, etc.) OR + // - Has a name/id that suggests it's for content (like PasteBin's paste_code) + return width > 150 || + (minLength && minLength > 30) || + (input.maxLength && input.maxLength > 50) || + (input.size && input.size > 20) || + (input.placeholder && input.placeholder.length > 15) || + /description|bio|comment|message|experience|summary|story|essay|feedback|review|content|code|paste|text|body|article|post|note/i.test(input.placeholder || input.name || input.id || input.className || ''); + } + + return false; +} + +function addAssistanceButton(formField: HTMLTextAreaElement | HTMLInputElement) { + // Skip if button already exists for this field + if (formAssistanceButtons.has(formField)) { + console.log("ScreenHawk: Button already exists for field:", formField); + return; + } + + // Skip if field is disabled or readonly + if (formField.disabled || formField.readOnly) { + console.log("ScreenHawk: Skipping disabled/readonly field:", formField); + return; + } + + console.log("ScreenHawk: Adding assistance button to field:", formField); + + // Create the assistance button + const button = document.createElement('button'); + button.innerHTML = '✨'; // Magic wand emoji + button.title = 'Get AI assistance for this field'; + button.type = 'button'; // Prevent form submission + button.style.cssText = ` + position: absolute; + top: 8px; + right: 8px; + width: 24px; + height: 24px; + border: none; + border-radius: 4px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + font-size: 12px; + cursor: pointer; + z-index: 10000; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + font-family: Arial, sans-serif; + opacity: 0.8; + pointer-events: auto; + `; + + // Add hover effect + button.addEventListener('mouseenter', () => { + button.style.transform = 'scale(1.1)'; + button.style.boxShadow = '0 4px 8px rgba(0,0,0,0.2)'; + button.style.opacity = '1'; + }); + + button.addEventListener('mouseleave', () => { + button.style.transform = 'scale(1)'; + button.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'; + button.style.opacity = '0.8'; + }); + + // Show/hide button based on field focus + formField.addEventListener('focus', () => { + button.style.opacity = '1'; + }); + + formField.addEventListener('blur', () => { + setTimeout(() => { + if (!button.matches(':hover')) { + button.style.opacity = '0.8'; + } + }, 100); + }); + + // Improved positioning strategy + const positionButton = () => { + try { + const fieldRect = formField.getBoundingClientRect(); + const fieldStyle = window.getComputedStyle(formField); + + // Try to find the best positioning strategy + let parentElement = formField.offsetParent as HTMLElement || formField.parentElement || document.body; + + // If field has position relative/absolute, we can position relative to it + if (fieldStyle.position === 'relative' || fieldStyle.position === 'absolute') { + parentElement = formField; + button.style.top = '8px'; + button.style.right = '8px'; + button.style.left = 'auto'; + button.style.bottom = 'auto'; + } else { + // Position relative to the closest positioned parent + while (parentElement && parentElement !== document.body) { + const parentStyle = window.getComputedStyle(parentElement); + if (parentStyle.position !== 'static') { + break; + } + parentElement = parentElement.offsetParent as HTMLElement || parentElement.parentElement || document.body; + } + + // Make sure the parent has relative positioning if it's static + const parentStyle = window.getComputedStyle(parentElement); + if (parentStyle.position === 'static') { + parentElement.style.position = 'relative'; + } + + // Calculate position relative to the positioned parent + const parentRect = parentElement.getBoundingClientRect(); + + button.style.top = `${fieldRect.top - parentRect.top + 8}px`; + button.style.right = `${parentRect.right - fieldRect.right + 8}px`; + button.style.left = 'auto'; + button.style.bottom = 'auto'; + } + + console.log("ScreenHawk: Button positioned successfully"); + } catch (error) { + console.error("ScreenHawk: Error positioning button:", error); + // Fallback: position relative to body with fixed positioning + const fieldRect = formField.getBoundingClientRect(); + button.style.position = 'fixed'; + button.style.top = `${fieldRect.top + 8}px`; + button.style.right = `${window.innerWidth - fieldRect.right + 8}px`; + button.style.left = 'auto'; + button.style.bottom = 'auto'; + } + }; + + // Initial positioning + positionButton(); + + // Update position on resize and scroll + const updatePosition = () => { + if (document.contains(formField) && document.contains(button)) { + positionButton(); + } + }; + + window.addEventListener('resize', updatePosition); + window.addEventListener('scroll', updatePosition, true); + + // Add click handler + button.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + currentFormField = formField; + showFormAssistanceDialog(formField); + }); + + // Find the best parent to append the button to + let buttonParent = formField.offsetParent as HTMLElement || formField.parentElement || document.body; + + // Append button to the parent element + buttonParent.appendChild(button); + + // Store the button reference + formAssistanceButtons.set(formField, button); + + console.log("ScreenHawk: Button added successfully to parent:", buttonParent); + + // Clean up when form field is removed + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.removedNodes.forEach((node) => { + if (node === formField || (node as Element).contains?.(formField)) { + observer.disconnect(); + window.removeEventListener('resize', updatePosition); + window.removeEventListener('scroll', updatePosition, true); + removeAssistanceButton(formField); + } + }); + }); + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); +} + +function removeAssistanceButton(formField: HTMLElement) { + const button = formAssistanceButtons.get(formField); + if (button && button.parentElement) { + button.parentElement.removeChild(button); + } + formAssistanceButtons.delete(formField); +} + +function getFormFieldContext(formField: HTMLTextAreaElement | HTMLInputElement): string { + let context = ''; + + // Get placeholder text + if (formField.placeholder) { + context += `Field placeholder: "${formField.placeholder}". `; + } + + // Get associated label + let label = ''; + if (formField.id) { + const labelElement = document.querySelector(`label[for="${formField.id}"]`); + if (labelElement) { + label = labelElement.textContent?.trim() || ''; + } + } + + // If no label found by ID, look for labels that contain this field + if (!label) { + const parentLabel = formField.closest('label'); + if (parentLabel) { + label = parentLabel.textContent?.replace(formField.textContent || '', '').trim() || ''; + } + } + + // Look for nearby text that might be a label + if (!label) { + const prevSibling = formField.previousElementSibling; + if (prevSibling && (prevSibling.tagName === 'SPAN' || prevSibling.tagName === 'DIV' || prevSibling.tagName === 'P')) { + const text = prevSibling.textContent?.trim(); + if (text && text.length < 100) { + label = text; + } + } + } + + if (label) { + context += `Field label: "${label}". `; + } + + // Get form name or title if available + const form = formField.closest('form'); + if (form) { + const formTitle = form.querySelector('h1, h2, h3, h4, h5, h6'); + if (formTitle) { + context += `Form section: "${formTitle.textContent?.trim()}". `; + } + } + + // Get field name/id hints + if (formField.name) { + context += `Field name: "${formField.name}". `; + } + + return context.trim(); +} + +function showFormAssistanceDialog(formField: HTMLTextAreaElement | HTMLInputElement) { + const context = getFormFieldContext(formField); + + // Determine if field is small or large to decide interface style + const fieldRect = formField.getBoundingClientRect(); + const isSmallField = fieldRect.height < 60 || formField.tagName.toLowerCase() === 'input'; + + if (isSmallField) { + showInlinePromptForSmallField(formField, context); + } else { + showInlinePromptForLargeField(formField, context); + } +} + +function showInlinePromptForSmallField(formField: HTMLTextAreaElement | HTMLInputElement, context: string) { + // Create a compact inline prompt that appears next to the field + const promptContainer = document.createElement('div'); + promptContainer.style.cssText = ` + position: absolute; + top: 100%; + left: 0; + right: 0; + background: white; + border: 2px solid #667eea; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + z-index: 10001; + padding: 12px; + margin-top: 4px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + min-width: 280px; + `; + + promptContainer.innerHTML = ` +
+ ✨ + AI Assistant +
+ ${context ? `
${context}
` : ''} + +
+ + +
+ `; + + // Position the container relative to the field + const fieldParent = formField.offsetParent as HTMLElement || formField.parentElement || document.body; + + // Ensure proper positioning context + if (window.getComputedStyle(fieldParent).position === 'static') { + fieldParent.style.position = 'relative'; + } + + fieldParent.appendChild(promptContainer); + + // Focus the input + const promptInput = promptContainer.querySelector('#quickPrompt') as HTMLInputElement; + setTimeout(() => promptInput.focus(), 100); + + setupInlinePromptHandlers(promptContainer, formField, context, 'quick'); +} + +function showInlinePromptForLargeField(formField: HTMLTextAreaElement | HTMLInputElement, context: string) { + // Create an expanded inline prompt that appears above the field + const promptContainer = document.createElement('div'); + promptContainer.style.cssText = ` + position: absolute; + bottom: 100%; + left: 0; + right: 0; + background: white; + border: 2px solid #667eea; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + z-index: 10001; + padding: 16px; + margin-bottom: 4px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + min-width: 320px; + `; + + promptContainer.innerHTML = ` +
+ ✨ + AI Writing Assistant +
+ ${context ? `
Context: ${context}
` : ''} + + +
+ + +
+ `; + + // Position the container relative to the field + const fieldParent = formField.offsetParent as HTMLElement || formField.parentElement || document.body; + + // Ensure proper positioning context + if (window.getComputedStyle(fieldParent).position === 'static') { + fieldParent.style.position = 'relative'; + } + + fieldParent.appendChild(promptContainer); + + // Focus the input + const promptInput = promptContainer.querySelector('#extendedPrompt') as HTMLTextAreaElement; + setTimeout(() => promptInput.focus(), 100); + + setupInlinePromptHandlers(promptContainer, formField, context, 'extended'); +} + +function setupInlinePromptHandlers( + container: HTMLElement, + formField: HTMLTextAreaElement | HTMLInputElement, + context: string, + type: 'quick' | 'extended' +) { + const promptInput = container.querySelector(`#${type}Prompt`) as HTMLInputElement | HTMLTextAreaElement; + const generateButton = container.querySelector(`#generate${type === 'quick' ? 'Quick' : 'Extended'}Content`) as HTMLButtonElement; + const cancelButton = container.querySelector(`#cancel${type === 'quick' ? 'Quick' : 'Extended'}Prompt`) as HTMLButtonElement; + const buttonText = container.querySelector(`#${type}ButtonText`) as HTMLSpanElement; + const loadingSpinner = container.querySelector(`#${type}LoadingSpinner`) as HTMLSpanElement; + + const closeContainer = () => { + if (container.parentElement) { + container.parentElement.removeChild(container); + } + }; + + const resetButtonState = () => { + generateButton.disabled = false; + buttonText.style.display = 'inline'; + loadingSpinner.style.display = 'none'; + generateButton.style.cursor = 'pointer'; + }; + + generateButton.addEventListener('click', () => { + const userPrompt = promptInput.value.trim(); + if (!userPrompt) { + alert('Please describe what you want to write.'); + promptInput.focus(); + return; + } + + // Show loading state + generateButton.disabled = true; + buttonText.style.display = 'none'; + loadingSpinner.style.display = 'inline'; + generateButton.style.cursor = 'wait'; + + // Create a comprehensive prompt for GPT + let fullPrompt = `You are helping a user fill out a form field. `; + if (context) { + fullPrompt += `Context about the form field: ${context} `; + } + fullPrompt += `The user wants: ${userPrompt}. Please provide ONLY the text content that should go in the form field, without any additional explanation or formatting. Keep it appropriate for the context and purpose of the field.`; + + console.log("Sending form assistance request to OpenAI:", fullPrompt); + currentFormField = formField; // Set current form field for filling + + chrome.runtime.sendMessage({ + action: "sendToOpenAI", + prompt: fullPrompt, + isFormAssistance: true + }, (response) => { + console.log("Response from background script:", response); + if (chrome.runtime.lastError) { + console.error("Error:", chrome.runtime.lastError); + alert("Sorry, there was an error generating content. Please check your OpenAI API key and try again."); + resetButtonState(); + } else { + closeContainer(); + } + }); + }); + + cancelButton.addEventListener('click', closeContainer); + + // Close on escape key + const escapeHandler = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + closeContainer(); + document.removeEventListener('keydown', escapeHandler); + } + }; + document.addEventListener('keydown', escapeHandler); + + // Close when clicking outside + const clickOutsideHandler = (e: Event) => { + if (!container.contains(e.target as Node) && !formField.contains(e.target as Node)) { + closeContainer(); + document.removeEventListener('click', clickOutsideHandler); + document.removeEventListener('keydown', escapeHandler); + } + }; + setTimeout(() => { + document.addEventListener('click', clickOutsideHandler); + }, 100); + + // Allow enter to submit (but not for textarea prompts with shift+enter) + promptInput.addEventListener('keydown', (e: Event) => { + const keyEvent = e as KeyboardEvent; + if (keyEvent.key === 'Enter' && (type === 'quick' || !keyEvent.shiftKey)) { + e.preventDefault(); + generateButton.click(); + } + }); +} + +function fillFormField(content: string) { + if (!currentFormField) { + console.error("No current form field to fill"); + return; + } + + // Clear any existing value + currentFormField.value = ''; + + // Animate typing effect for better UX + let index = 0; + const typewriterSpeed = 20; // milliseconds per character + + const typeWriter = () => { + if (index < content.length) { + currentFormField!.value += content.charAt(index); + index++; + + // Trigger input event for each character (for reactive frameworks) + const inputEvent = new Event('input', { bubbles: true }); + currentFormField!.dispatchEvent(inputEvent); + + setTimeout(typeWriter, typewriterSpeed); + } else { + // Final events after typing is complete + const events = ['change', 'blur']; + events.forEach(eventType => { + const event = new Event(eventType, { bubbles: true }); + currentFormField!.dispatchEvent(event); + }); + + // Show a subtle success indication + const button = formAssistanceButtons.get(currentFormField!); + if (button) { + const originalText = button.innerHTML; + button.innerHTML = '✅'; + button.style.background = 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)'; + + setTimeout(() => { + button.innerHTML = originalText; + button.style.background = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'; + }, 2000); + } + } + }; + + // Focus the field and start typing animation + currentFormField.focus(); + typeWriter(); + + // Clear the current field reference + currentFormField = null; +} + +// Magic wand functionality for manual field selection +function activateMagicWandMode() { + if (magicWandMode) { + deactivateMagicWandMode(); + return; + } + + console.log("ScreenHawk: Activating magic wand mode"); + magicWandMode = true; + + // Find all potential text input fields (more permissive than automatic detection) + const allTextFields = document.querySelectorAll('textarea, input[type="text"], input:not([type]), input[type=""], input[type="search"], input[type="email"], input[type="url"]'); + + // Create overlay for instructions + magicWandOverlay = document.createElement('div'); + magicWandOverlay.style.cssText = ` + position: fixed; + top: 20px; + left: 50%; + transform: translateX(-50%); + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 12px 24px; + border-radius: 8px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + font-weight: 500; + box-shadow: 0 4px 20px rgba(0,0,0,0.15); + z-index: 10001; + display: flex; + align-items: center; + gap: 8px; + border: 2px solid rgba(255,255,255,0.2); + `; + magicWandOverlay.innerHTML = '🪄 Magic Wand Mode: Click on any text field to add AI assistance • Press Escape to exit'; + document.body.appendChild(magicWandOverlay); + + // Highlight all potential fields + allTextFields.forEach((field) => { + const element = field as HTMLElement; + if (isFieldVisible(element)) { + highlightField(element); + } + }); + + // Add escape key listener + document.addEventListener('keydown', magicWandEscapeHandler); +} + +function deactivateMagicWandMode() { + console.log("ScreenHawk: Deactivating magic wand mode"); + magicWandMode = false; + + // Remove overlay + if (magicWandOverlay && magicWandOverlay.parentElement) { + magicWandOverlay.parentElement.removeChild(magicWandOverlay); + magicWandOverlay = null; + } + + // Remove highlights + highlightedFields.forEach((field) => { + removeHighlight(field); + }); + highlightedFields = []; + + // Remove escape key listener + document.removeEventListener('keydown', magicWandEscapeHandler); +} + +function magicWandEscapeHandler(e: KeyboardEvent) { + if (e.key === 'Escape') { + deactivateMagicWandMode(); + } +} + +function isFieldVisible(element: HTMLElement): boolean { + const style = window.getComputedStyle(element); + const rect = element.getBoundingClientRect(); + + return style.display !== 'none' && + style.visibility !== 'hidden' && + style.opacity !== '0' && + rect.width > 0 && + rect.height > 0 && + rect.top < window.innerHeight && + rect.bottom > 0 && + rect.left < window.innerWidth && + rect.right > 0; +} + +function highlightField(field: HTMLElement) { + // Skip if already highlighted or has a button + if (highlightedFields.includes(field) || formAssistanceButtons.has(field)) { + return; + } + + highlightedFields.push(field); + + // Store original styles + const originalBorder = field.style.border; + const originalBoxShadow = field.style.boxShadow; + const originalTransition = field.style.transition; + + // Apply highlight styles + field.style.transition = 'all 0.3s ease'; + field.style.border = '2px solid #667eea'; + field.style.boxShadow = '0 0 10px rgba(102, 126, 234, 0.3), inset 0 0 10px rgba(102, 126, 234, 0.1)'; + + // Add click handler for magic wand mode + const clickHandler = (e: Event) => { + e.preventDefault(); + e.stopPropagation(); + + if (magicWandMode) { + // Add AI assistance to this field + addAssistanceButton(field as HTMLTextAreaElement | HTMLInputElement); + + // Remove highlight + removeHighlight(field); + + // Show success feedback + showSuccessFeedback(field); + } + }; + + field.addEventListener('click', clickHandler); + + // Store cleanup data + (field as any).__magicWandData = { + originalBorder, + originalBoxShadow, + originalTransition, + clickHandler + }; +} + +function removeHighlight(field: HTMLElement) { + const magicWandData = (field as any).__magicWandData; + if (!magicWandData) return; + + // Restore original styles + field.style.border = magicWandData.originalBorder; + field.style.boxShadow = magicWandData.originalBoxShadow; + field.style.transition = magicWandData.originalTransition; + + // Remove click handler + field.removeEventListener('click', magicWandData.clickHandler); + + // Clean up + delete (field as any).__magicWandData; + + // Remove from highlighted list + const index = highlightedFields.indexOf(field); + if (index > -1) { + highlightedFields.splice(index, 1); + } +} + +function showSuccessFeedback(field: HTMLElement) { + const feedback = document.createElement('div'); + feedback.style.cssText = ` + position: absolute; + top: -40px; + left: 50%; + transform: translateX(-50%); + background: #22c55e; + color: white; + padding: 8px 16px; + border-radius: 6px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 12px; + font-weight: 500; + z-index: 10002; + box-shadow: 0 2px 10px rgba(0,0,0,0.15); + pointer-events: none; + opacity: 0; + transition: opacity 0.3s ease; + `; + feedback.textContent = '✅ AI assistance added!'; + + // Position relative to field + const fieldRect = field.getBoundingClientRect(); + feedback.style.position = 'fixed'; + feedback.style.top = `${fieldRect.top - 40}px`; + feedback.style.left = `${fieldRect.left + fieldRect.width / 2}px`; + + document.body.appendChild(feedback); + + // Animate in + setTimeout(() => { + feedback.style.opacity = '1'; + }, 10); + + // Remove after delay + setTimeout(() => { + feedback.style.opacity = '0'; + setTimeout(() => { + if (feedback.parentElement) { + feedback.parentElement.removeChild(feedback); + } + }, 300); + }, 2000); +} + +// Add keyboard shortcut for magic wand mode +document.addEventListener('keydown', (e) => { + // Ctrl+Shift+Z or Cmd+Shift+Z for magic wand + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'Z') { + e.preventDefault(); + activateMagicWandMode(); + } +}); \ No newline at end of file diff --git a/src/popup.ts b/src/popup.ts index 4ef093c..0d93e5f 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -4,6 +4,8 @@ document.addEventListener('DOMContentLoaded', () => { const captureButton = document.getElementById('captureButton'); + const magicWandButton = document.getElementById('magicWandButton'); + if (captureButton) { captureButton.addEventListener('click', () => { chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { @@ -40,19 +42,41 @@ document.addEventListener('DOMContentLoaded', () => { } else { console.error('Capture button not found'); } -}); - -chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { - const activeTab = tabs[0]; - if (activeTab && activeTab.id) { - chrome.tabs.sendMessage(activeTab.id, {action: "prepareCapture"}, (response) => { - if (chrome.runtime.lastError) { - console.error("Error sending message:", chrome.runtime.lastError); - } else { - console.log("Message sent successfully, response:", response); - } + + if (magicWandButton) { + magicWandButton.addEventListener('click', () => { + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + const activeTab = tabs[0]; + if (activeTab && activeTab.id) { + chrome.scripting.executeScript( + { + target: { tabId: activeTab.id }, + files: ['content.js'] + }, + () => { + if (chrome.runtime.lastError) { + console.error('Script injection failed:', chrome.runtime.lastError); + return; + } + chrome.tabs.sendMessage(activeTab.id!, { action: "activateMagicWand" }, (response) => { + if (chrome.runtime.lastError) { + console.error('Error:', JSON.stringify(chrome.runtime.lastError)); + alert(`Error: ${chrome.runtime.lastError.message}`); + } else { + console.log('Magic wand activated successfully'); + console.log('Response:', JSON.stringify(response)); + window.close(); + } + }); + } + ); + } else { + console.error('No active tab found'); + alert('No active tab found'); + } + }); }); } else { - console.error("No active tab found"); + console.error('Magic wand button not found'); } }); \ No newline at end of file