Chrome拡張の「システムエラー文調査ロボ」をGPTでつくってみた

# 概要
エラー文をGPTに調べてもらうと、割りとよしなに空気を読んで原因と解決策を回答してくれるので、もうひと手間ラクにできたらということで拡張機能にしてみました。

# 作ったもの
* Chromeの拡張機能
* GPTのAPIKEYを登録することでつかえる
* エラー文を画面上でドラックすると、右クリックでGPTに調査をかけられる
* 原因と解決策が提示される
* 拡張機能のボタンから開いて使うことも可能（右クリック以外でも利用）

## こんなの

### 拡張アイコンから使うとき
入力してまんまつかってください
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215128/7a9d574a-99df-3160-9310-ee5be2161c73.png)

### エラー文ドラッグ→右クリックで使うとき
右クリックで選択したエラー文をそのまま使うこともできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215128/73c67db0-b93e-a73a-235f-868b734eef11.png)

### 調査結果はこんな感じ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215128/b3808687-db62-c4d8-d64d-2cb7082a0284.png)

# 構成
手抜き感満載なので、プロジェクト直下に脳死でファイルが並んでいます。

```
プロジェクトディレクトリ
∟　background.js
∟　content.js
∟　icon128.png
∟　manifest.json
∟　options.html
∟　options.js
∟　popup.html
∟　popup.js
```

# 各記載内容
## background.js

```javascript
let selectedText = "";

chrome.runtime.onInstalled.addListener(function() {
    chrome.contextMenus.create({
      id: "investigate",
      title: "エラー内容を調査",
      contexts: ["selection"]
    });
});

chrome.contextMenus.onClicked.addListener(function(info, tab) {
    if (info.menuItemId === "investigate") {
      selectedText = info.selectionText;
      chrome.tabs.create({url: "popup.html"});
    }
});

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) {
    if (request.method == "getSelectedText")
        sendResponse({data: selectedText});
});

```

## content.js

途中で要らなくなった気がする

```javascript
let selectedText = window.getSelection().toString();
chrome.runtime.sendMessage({text: selectedText});
```

## icon128.png

（よしなにどうぞ）

## manifest.json

```
{
    "name": "システムエラー文調査ロボ",
    "version": "1.0",
    "description": "エラー文に対してGPTを用いて調査・回答を提示します",
    "manifest_version": 3,
    "permissions": ["contextMenus", "activeTab", "storage"],
    "background": {
      "service_worker": "background.js"
    },
    "action": {
      "default_popup": "popup.html"
    },
    "options_page": "options.html",
    "content_scripts": [
      {
        "matches": ["<all_urls>"],
        "js": ["content.js"]
      }
    ],
    "icons": {
        "128": "icon128.png"
    }
}
```
## options.html

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <div class="container" style="max-width: 800px;">
    <!-- Header -->
    <header class="mt-3 mb-4">
      <h1 class="text-center">APIキー設定</h1>
    </header>

    <!-- Body -->
    <main>
      <div class="mb-4">
        <label for="apiKey">API Key:</label>
        <input type="text" id="apiKey" name="apiKey" class="form-control">
        <br>
        <button id="saveButton" class="btn btn-primary">APIキーを登録</button>
      </div>
    </main>

    <!-- Footer -->
    <footer class="mt-5 text-center">
      <p>&copy; 2023 Error Investigation Robota</p>
    </footer>
  </div>
  
  <script src="options.js"></script>
</body>
</html>
```

## options.js

```javascript
document.getElementById('saveButton').onclick = function(){
    let apiKey = document.getElementById('apiKey').value;
    chrome.storage.sync.set({"apiKey": apiKey}, function() {
      console.log('Value is set to ' + apiKey);
      alert('APIキーを登録しました。');
    });
  }
```

## popup.html

```html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
  <div class="container" style="max-width: 1200px; min-width:800px;">
    <!-- Header -->
    <header class="mt-3 mb-4">
      <h1 class="text-center">システムエラー文調査ロボ</h1>
    </header>

    <!-- Body -->
    <main>
      <div class="mb-4">
        <p class="text-center">以下のエラー文の原因と対応策について調査回答します。</p>
        <div class="form-group">
          <input type="text" class="form-control" id="input_error" placeholder="エラー文をここに入力">
          <button class="btn btn-primary mt-2" id="submit_button">調査開始</button>
        </div>
      </div>

      <div class="card">
        <div class="card-body">
          <p class="card-text">
            <strong>調査対象エラー文：</strong>
            <span id="question_error"></span>
          </p>
          <p><strong>調査結果：</strong></p>
          <div id="result" class="card-text">
          </div>
          <div style="text-align:center;">
            <div id="loading" class="spinner-border" role="status" style="display:none;">
              <span class="sr-only">Loading...</span>
            </div>
          </div>
        </div>
      </div>

      <div>
        <p style="text-align:right; margin:15px 0;">
          <a href="options.html" target="_blank">APIキー設定</a>
        </p>
      </div>
    </main>

    <!-- Footer -->
    <footer class="mt-5 text-center">
      <p>&copy; 2023 Error Investigation Robota</p>
    </footer>

    <script src="popup.js"></script>
  </div>
</body>
</html>

```

## popup.js

```javascript
 function investigateError(selectedText) {
    document.getElementById('loading').style.display = 'inline-block';
    document.getElementById('input_error').value = selectedText;
    document.getElementById('question_error').innerText = selectedText;
    const promptMessage = {
      role: "user",
      content: "次のエラー文の原因と対応方法について回答してください。「" + selectedText + "」"
    };

    // Get API key from storage
    chrome.storage.sync.get(["apiKey"], function(result) {
      const apiKey = result.apiKey;
      console.log(apiKey);
      fetch('https://api.openai.com/v1/chat/completions', {
        method: 'POST',
        headers: {
            'Authorization': 'Bearer ' + apiKey,
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          //   'model': 'gpt-4',
            'model': 'gpt-3.5-turbo',
            'max_tokens': 1000,
            'temperature': 1,
            'top_p': 1,
            'n': 1,
            'stream': false,
            'messages': [promptMessage],
        })
      }).then(response => {
          if (!response.ok) {
              throw new Error('Network response was not ok');
          }
          return response.json();
      })
      .then(data => {
          console.log(data);
          if (data.error) {
              throw new Error(data.error);
          } else if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) {
              document.getElementById('result').innerText = data.choices[0].message.content;
          } else {
              document.getElementById('result').innerText = "回答が見つかりませんでした。";
          }
          document.getElementById('loading').style.display = 'none';
      })
      .catch(error => {
          console.error('There has been a problem with your fetch operation:', error);
          alert('エラーが発生しました: ' + error.message);
      });
      
    });
}

document.getElementById('submit_button').addEventListener('click', function() {
    const selectedText = document.getElementById('input_error').value;
    investigateError(selectedText);
});

chrome.runtime.sendMessage({method: "getSelectedText"}, function(response) {
    const selectedText = response.data;
    if (selectedText) {
        investigateError(selectedText);
    }
});

```

# おわり

ちなみにですが、ソースコードもほぼChatGPTにつくらせました。所要時間は３０分ほど。とんでもねえなこいつは。
いずれやり取りのプロンプトなども公開しようと思います。
荒いところはあるかと思いますが、そのへんも加味してご了承いただけると幸いです。