Chat AI(完全無料)を使ってブラウザの拡張機能を10分で作る





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:

  1. Save all these files in their respective locations.
  2. Create the placeholder icon files in the icons/ directory.
  3. 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… > select manifest.json).
  4. 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 ボタンが追加した各検索エンジンに付いていて、個別に無効化できるようになっている。

context-search-menu-item context-search-options-page

まとめ

最初に与える仕様をできる限り細かく書いておけば、AIがコードを生成してる間にアイコンを揃えて10分で拡張機能が作成できる。すごい



コメント