Chat AI(完全無料)を使ってブラウザの拡張機能を作る。
Chat AI は何でもいいけど、Google のAI Studio (現時点でのバージョンは Gemini 2.5 Pro Preview 05-06)を使っているので以下これを使います。AI を使って、なるべく一発で作ることを目指すので、細かい仕様まで考えることが重要。
例として、コンテキストメニューを使ってウェブページ内の選択文字列を検索する拡張機能を作成します。
細かい仕様まで、決めて AI に生成してもらう
以下の内容を AI Studio の入力してコードを生成してもらう。
ウェブページの選択した文字列をコンテキストメニューを使って検索するブラウザ拡張機能を作成して。
* 拡張機能名は context-search にして。
* manifest version 3 に準拠して。
* ディレクトリ構成は以下のようにして。
.
├── css
│ └── options.css
├── icons
├── js
│ └── background.js
├── .gitignore
├── Makefile
├── manifest.json
├── options.html
└── README.md
* Firefox 向けなので browser_specific_settings を manifest.json に追加して。
"browser_specific_settings": {
"gecko": {
"id": "{665ac0e6-53d2-4e3b-bca9-83331ff406eb}",
"strict_min_version": "115.0"
}
}
* Firefox 向けなので background は scripts にして。
"background": {
"scripts": []
}
* オプションページで新たな検索エンジンを追加できるようにして。また、選択した文字列を %s で置換するようにして。
* 初期設定で https://duckduckgo.com/?q=%s を検索エンジンとして設定して。
* README.md を英語で書いて。
* .gitignore を書いて。
出力結果
Okay, here’s the structure and code for your “context-search” Firefox browser extension.
First, let’s create the directory structure and files:
.
├── css
│ └── options.css
├── icons
│ ├── icon-16.png
│ ├── icon-32.png
│ ├── icon-48.png
│ └── icon-128.png
├── js
│ ├── background.js
│ └── options.js
├── .gitignore
├── Makefile
├── manifest.json
├── options.html
└── README.md
1. Icons (icons/) You’ll need to create these icon
files. For placeholder purposes, you can use simple colored squares or
find some free-to-use icons. I’ll assume you’ll create: *
icons/icon-16.png
* icons/icon-32.png
*
icons/icon-48.png
* icons/icon-128.png
2. manifest.json
{
"manifest_version": 3,
"name": "context-search",
"version": "1.0.0",
"description": "Search selected text using configurable search engines via the context menu.",
"permissions": [
"contextMenus",
"storage"
],
"icons": {
"16": "icons/icon-16.png",
"32": "icons/icon-32.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
},
"background": {
"scripts": ["js/background.js"]
},
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"browser_specific_settings": {
"gecko": {
"id": "{665ac0e6-53d2-4e3b-bca9-83331ff406eb}",
"strict_min_version": "115.0"
}
}
}
3. js/background.js
const DEFAULT_ENGINES_KEY = 'searchEngines';
const PARENT_MENU_ID = 'context-search-parent';
const DEFAULT_ENGINES = [
{ id: 'duckduckgo', name: 'DuckDuckGo', url: 'https://duckduckgo.com/?q=%s', enabled: true }
];
async function getSearchEngines() {
const result = await browser.storage.sync.get(DEFAULT_ENGINES_KEY);
return result[DEFAULT_ENGINES_KEY] || DEFAULT_ENGINES;
}
async function saveSearchEngines(engines) {
await browser.storage.sync.set({ [DEFAULT_ENGINES_KEY]: engines });
}
async function createContextMenuItems() {
await browser.contextMenus.removeAll(); // Clear existing items
const engines = await getSearchEngines();
const enabledEngines = engines.filter(engine => engine.enabled);
if (enabledEngines.length === 0) {
// No enabled engines, maybe add a disabled parent or nothing
console.log("No enabled search engines. Context menu will not be created.");
return;
}
if (enabledEngines.length === 1) {
// If only one engine, create it directly without a parent
const engine = enabledEngines[0];
browser.contextMenus.create({
id: `search-engine-${engine.id}`,
title: `Search "${engine.name}" for "%s"`,
contexts: ["selection"]
});
} else {
// Multiple engines, create a parent menu
browser.contextMenus.create({
id: PARENT_MENU_ID,
title: "Search with context-search",
contexts: ["selection"]
});
for (const engine of enabledEngines) {
browser.contextMenus.create({
id: `search-engine-${engine.id}`,
parentId: PARENT_MENU_ID,
title: engine.name,
contexts: ["selection"]
});
}
}
}
browser.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId.startsWith('search-engine-')) {
const engineId = info.menuItemId.replace('search-engine-', '');
const engines = await getSearchEngines();
const engine = engines.find(e => e.id === engineId);
if (engine && info.selectionText) {
const queryUrl = engine.url.replace('%s', encodeURIComponent(info.selectionText));
browser.tabs.create({
url: queryUrl,
index: tab ? tab.index + 1 : undefined // Open next to current tab if possible
});
}
}
});
// Listen for storage changes to update context menu
browser.storage.onChanged.addListener((changes, areaName) => {
if (areaName === 'sync' && changes[DEFAULT_ENGINES_KEY]) {
createContextMenuItems();
}
});
// Initialize
async function initialize() {
const engines = await getSearchEngines();
if (!engines || engines.length === 0) { // First run or cleared storage
await saveSearchEngines(DEFAULT_ENGINES);
}
createContextMenuItems();
}
// Create menu items on startup and install
browser.runtime.onStartup.addListener(createContextMenuItems);
browser.runtime.onInstalled.addListener(initialize);
initialize(); // Also run on first load after enabling
4. options.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Context Search Options</title>
<link rel="stylesheet" href="css/options.css">
</head>
<body>
<h1>Context Search Engines</h1>
<div id="engines-list">
<!-- Search engines will be listed here by JavaScript -->
</div>
<hr>
<h2>Add New Search Engine</h2>
<form id="add-engine-form">
<div>
<label for="engine-name">Name:</label>
<input type="text" id="engine-name" required placeholder="e.g., Google">
</div>
<div>
<label for="engine-url">URL (use %s for query):</label>
<input type="text" id="engine-url" required placeholder="e.g., https://www.google.com/search?q=%s">
</div>
<button type="submit">Add Engine</button>
</form>
<p class="note">The <code>%s</code> in the URL will be replaced with the selected text.</p>
<script src="js/options.js"></script>
</body>
</html>
5. css/options.css
body {
font-family: sans-serif;
margin: 20px;
background-color: #f4f4f4;
color: #333;
min-width: 400px;
}
h1, h2 {
color: #333;
border-bottom: 1px solid #ccc;
padding-bottom: 10px;
}
#engines-list {
margin-bottom: 20px;
}
.engine-item {
background-color: #fff;
border: 1px solid #ddd;
padding: 10px 15px;
margin-bottom: 10px;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.engine-item .details {
flex-grow: 1;
}
.engine-item .name {
font-weight: bold;
display: block;
margin-bottom: 5px;
}
.engine-item .url {
font-size: 0.9em;
color: #555;
word-break: break-all;
}
.engine-item button {
background-color: #e74c3c;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
margin-left: 10px;
}
.engine-item button.toggle-enable {
background-color: #2ecc71; /* Green for enable */
}
.engine-item button.toggle-enable.disabled {
background-color: #f39c12; /* Orange for disable */
}
.engine-item button:hover {
opacity: 0.9;
}
form div {
margin-bottom: 10px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input[type="text"] {
width: calc(100% - 22px); /* Full width minus padding and border */
padding: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
button[type="submit"] {
background-color: #3498db;
color: white;
border: none;
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-size: 1em;
}
button[type="submit"]:hover {
background-color: #2980b9;
}
hr {
border: 0;
height: 1px;
background: #ccc;
margin: 20px 0;
}
.note {
font-size: 0.9em;
color: #666;
margin-top: 10px;
}
6. js/options.js
const enginesList = document.getElementById('engines-list');
const addEngineForm = document.getElementById('add-engine-form');
const engineNameInput = document.getElementById('engine-name');
const engineUrlInput = document.getElementById('engine-url');
const DEFAULT_ENGINES_KEY = 'searchEngines'; // Same as in background.js
async function getSearchEngines() {
const result = await browser.storage.sync.get(DEFAULT_ENGINES_KEY);
// Provide a default empty array if nothing is stored
return result[DEFAULT_ENGINES_KEY] || [];
}
async function saveSearchEngines(engines) {
await browser.storage.sync.set({ [DEFAULT_ENGINES_KEY]: engines });
renderEngines(); // Re-render after saving
}
function generateId() {
return Date.now().toString(36) + Math.random().toString(36).substring(2);
}
function renderEngines() {
getSearchEngines().then(engines => {
enginesList.innerHTML = ''; // Clear current list
if (engines.length === 0) {
enginesList.innerHTML = '<p>No search engines configured.</p>';
return;
}
engines.forEach((engine, index) => {
const item = document.createElement('div');
item.classList.add('engine-item');
item.dataset.id = engine.id;
const details = document.createElement('div');
details.classList.add('details');
const nameSpan = document.createElement('span');
nameSpan.classList.add('name');
nameSpan.textContent = engine.name;
details.appendChild(nameSpan);
const urlSpan = document.createElement('span');
urlSpan.classList.add('url');
urlSpan.textContent = engine.url;
details.appendChild(urlSpan);
item.appendChild(details);
const toggleButton = document.createElement('button');
toggleButton.classList.add('toggle-enable');
toggleButton.textContent = engine.enabled ? 'Disable' : 'Enable';
if (engine.enabled) {
toggleButton.classList.remove('disabled');
} else {
toggleButton.classList.add('disabled');
}
toggleButton.addEventListener('click', async () => {
const currentEngines = await getSearchEngines();
const targetEngine = currentEngines.find(e => e.id === engine.id);
if (targetEngine) {
targetEngine.enabled = !targetEngine.enabled;
await saveSearchEngines(currentEngines);
}
});
item.appendChild(toggleButton);
const removeButton = document.createElement('button');
removeButton.classList.add('remove');
removeButton.textContent = 'Remove';
removeButton.addEventListener('click', async () => {
let currentEngines = await getSearchEngines();
currentEngines = currentEngines.filter(e => e.id !== engine.id);
await saveSearchEngines(currentEngines);
});
item.appendChild(removeButton);
enginesList.appendChild(item);
});
});
}
addEngineForm.addEventListener('submit', async (event) => {
event.preventDefault();
const name = engineNameInput.value.trim();
const url = engineUrlInput.value.trim();
if (!name || !url) {
alert('Name and URL are required.');
return;
}
if (!url.includes('%s')) {
alert('URL must contain %s as a placeholder for the search query.');
return;
}
const newEngine = {
id: generateId(),
name: name,
url: url,
enabled: true // New engines are enabled by default
};
const engines = await getSearchEngines();
engines.push(newEngine);
await saveSearchEngines(engines);
engineNameInput.value = '';
engineUrlInput.value = '';
});
// Initial render
renderEngines();
7. .gitignore
# Packaged extension
*.zip
*.xpi
# Build artifacts
dist/
build/
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
yarn.lock
# Editor/OS files
.vscode/
.idea/
*.swp
*~
.DS_Store
Thumbs.db
8. Makefile
NAME=context-search
VERSION := $(shell grep '"version":' manifest.json | sed 's/.*"version": "\(.*\)",/\1/')
ZIP_FILE=dist/$(NAME)-$(VERSION).zip
XPI_FILE=dist/$(NAME)-$(VERSION).xpi
# Files and directories to include in the package
PACKAGE_FILES=manifest.json js css icons options.html
all: build
build: $(ZIP_FILE)
$(ZIP_FILE): $(PACKAGE_FILES) $(shell find $(PACKAGE_FILES) -type f)
@mkdir -p dist
@echo "Building $(ZIP_FILE)..."
@zip -r -FS $(ZIP_FILE) $(PACKAGE_FILES) -x "*.DS_Store"
@echo "Built: $(ZIP_FILE)"
# For Firefox, .xpi is just a .zip file with a different extension
# You can typically rename the .zip or use it directly for temporary loading.
# This target is mostly for semantic purposes or if a tool specifically expects .xpi
build-xpi: $(XPI_FILE)
$(XPI_FILE): $(ZIP_FILE)
@echo "Creating $(XPI_FILE) from $(ZIP_FILE)..."
@cp $(ZIP_FILE) $(XPI_FILE)
@echo "Created: $(XPI_FILE)"
clean:
@echo "Cleaning up..."
@rm -rf dist
@echo "Cleaned."
.PHONY: all build build-xpi clean
Note on Makefile: The
$(shell find $(PACKAGE_FILES) -type f)
part helps ensure
Makefile rebuilds if any file within the listed directories changes. The
-FS
flag for zip
is useful to sync file
timestamps if you’re zipping repeatedly, leading to more consistent zip
files.
9. README.md
# Context Search Extension
Context Search is a browser extension for Firefox that allows you to quickly search selected text on a webpage using your preferred search engines via the context menu.
## Features
* Search selected text using a right-click (context menu).
* Add, remove, and manage multiple search engines.
* Enable or disable specific search engines from appearing in the context menu.
* Uses `%s` as a placeholder for the selected text in search engine URLs.
* Default search engine: DuckDuckGo.
* Settings are synced across your Firefox instances (if sync is enabled).
## Installation
### From Mozilla Add-ons (AMO) - (Hypothetical)
Once published, you would typically install it from the official Firefox Add-ons website.
### Manual Installation (for development/testing)
1. Clone this repository or download the source code as a ZIP file and extract it.
```bash
git clone <repository_url>
cd context-search
```
2. If you downloaded a ZIP of the source, you can build the extension package:
```bash
make build
```
This will create a `dist/context-search-X.Y.Z.zip` file.
3. Open Firefox.
4. Type `about:debugging` into the URL bar and press Enter.
5. Click on "This Firefox" (or "This Nightly", etc.) in the sidebar.
6. Click "Load Temporary Add-on...".
7. Navigate to the directory where you cloned/extracted the extension and select the `manifest.json` file, OR select the `dist/context-search-X.Y.Z.zip` file created by the `make` command.
The extension icon should now appear in your browser's toolbar (if applicable, though this one is mainly context menu based) and its functionality will be active.
## How to Use
1. **Select Text**: On any webpage, select the text you want to search.
2. **Right-Click**: Right-click on the selected text to open the context menu.
3. **Search**:
* If you have multiple enabled search engines, you'll see a "Search with context-search" parent menu. Hover over it to see your list of configured search engines. Click on the desired engine.
* If you have only one search engine enabled, it will appear directly in the context menu as "Search [Engine Name] for '[selected text]'".
4. A new tab will open with the search results from your chosen search engine.
## Configuring Search Engines
1. Right-click the extension icon (if it appears in the toolbar/overflow menu) and select "Manage Extension", then go to the "Options" tab. Alternatively, go to `about:addons`, find "Context Search", click the three dots (...) menu, and select "Options".
2. The options page will display:
* A list of your current search engines.
* A form to add new search engines.
### Adding a New Search Engine
1. Enter a **Name** for the search engine (e.g., "Google", "Wikipedia").
2. Enter the **URL** for the search engine. Crucially, use `%s` where the selected text (your search query) should be inserted.
* Example for Google: `https://www.google.com/search?q=%s`
* Example for Wikipedia: `https://en.wikipedia.org/wiki/Special:Search?search=%s`
3. Click "Add Engine".
### Managing Existing Engines
For each engine listed, you can:
* **Enable/Disable**: Click the "Disable" (or "Enable") button to control whether this engine appears in the context menu. Disabled engines are kept in your list but won't be shown for searching.
* **Remove**: Click the "Remove" button to permanently delete the search engine from your list.
## Development
To make changes:
1. Edit the source files (`.js`, `.html`, `.css`).
2. If you loaded the extension via `manifest.json` in `about:debugging`:
* Click the "Reload" button for the "context-search" extension on the `about:debugging` page.
3. If you loaded a `.zip` file:
* Run `make build` to create a new zip.
* Remove the old version from `about:debugging` and load the new zip.
## Contributing
Contributions, bug reports, and feature requests are welcome! Please open an issue or submit a pull request on the project's repository.
## License
This project is open source, feel free to use and modify. (Consider adding a specific license like MIT if you plan to distribute widely).
Final Steps:
- Save all these files in their respective locations.
- Create the placeholder icon files in the
icons/
directory. - You can now test the extension by loading it into Firefox as a
temporary add-on (via
about:debugging
> This Firefox > Load Temporary Add-on… > selectmanifest.json
). - Use
make build
to create a distributable ZIP file.
This setup provides a robust starting point for your context search extension.
出力結果について
options.js をディレクトリ構成に含めてなかったけど(書き忘れ)自動で生成してくれているし、 Makefile も勝手に書いてくれている。
チャットの説明が英語なっているので、チャットは日本語で、コード内のコメントは英語でと書いておくといいかもしれない。
Firefox 向けの記述を無くせば、おそらくchrome 向けに background を
service_worker で作成してくれると思われる。また、IDの部分は UUID
なので、Linux コマンドで uuidgen -r で生成するか、DuckDuckGo で uuid
を検索すると生成してくれます。"gecko": { "id": "{665ac0e6-53d2-4e3b-bca9-83331ff406eb}",
これも Firefox用の記述。
拡張機能を試す
出力された README.md 内の Manual Installation (for development/testing) に書かれている通り、 about:debugging#/runtime/this-firefox から manifest.json を読み込んで試すことができる。
コードの内容は読んでないけど、コピペするだけで、まったくバグもなくて普通に使用できる。ただし、細かいところだけど、最初の検索エンジンを追加すると初期設定で追加されている duckduckgo を上書きする仕様になっていて、その後は検索エンジンを追加すると複数のコンテキストメニューが表示されるようになっている。また、仕様に書いていなかった disable ボタンが追加した各検索エンジンに付いていて、個別に無効化できるようになっている。
まとめ
最初に与える仕様をできる限り細かく書いておけば、AIがコードを生成してる間にアイコンを揃えて10分で拡張機能が作成できる。すごい
コメント
コメントを投稿