chromeのちょっとした拡張機能(備忘録)

## chromeのちょっとした拡張機能(備忘録)
#### **概要**
ニュース記事をもとにインフォグラフィックスを制作するという大学の課題として制作したchromeの拡張機能の備忘録として記録したもの。

#### **作品概要**
この[記事](https://www.washingtonpost.com/technology/2024/09/18/energy-ai-use-electricity-water-data-centers/)(生成AIを使用するときにサーバーの冷却や動作させる電力の発電などの過程で大量の水を消費しているという問題に関するもの)で指摘されている、GPT-4モデルで100wordのメールを生成するために519mlの水が消費されるという事実を可視化するための作品を制作した。

chatGPTを用いて、文章を生成させた文章の語数をカウントし、画面左上に生成された語数、右下に消費されたと推定される水量の合計、画面右上に水量をペットボトルの本数で表した図が表示される。

![作品スクショ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3631311/c339bbbd-a520-2570-e1f6-faa663b88071.png)
※実際にはデータセンターの場所により消費する水の量は異なり、また消費する水量は推定であり確実なものではない。詳細は[元論文](https://arxiv.org/abs/2304.03271)を参照。

#### **実装概要**
1. chromeの拡張機能を用いて、chatGPTの生成された文章が格納される`<div>`タグのclassの中身の語数をカウントする。
2. 100語につき、519ml水量が増えるように表示する。
3. 水量を500で割った商の数だけ、用意したペットボトルの画像を表示する。

#### **実装詳細**[プログラムファイル](https://github.com/camen89/chatGPT_extension)
- **chromeの拡張機能を作成する。**
    chromeの拡張機能の作成方法の詳細については他の方が解説されている記事([Chrome拡張機能の実装（入門から公開まで）](https://qiita.com/Nozomuts/items/fc1d4f8fc995d830817d))を参考にする。
    
    用意したファイルは以下の通り(ポップアップは実際には使わなかったのでなくてもよい、、、)
    ```
    background.js 　
    content.js     #実際にどのような動作をするかを書き込む
    manifest.json  #拡張機能に必須のファイル  
    popup.html
    popup.css
    popup.js
    ```
    manifest.jsonの中身
    ```json
    // manifest.json
    {
    "manifest_version": 3,
    "name": "ChatGPT文字数カウンター",
    "version": "5.0",
    "description": "ChatGPTの回答文字数をリアルタイムでカウントします.回答の和の水量を右下に表示します.水量をペットボトルで表示します",
    "icon": {

    },
    "permissions": [
        "scripting",
        "activeTab"
    ],
    "action": {
        "default_popup": "popup.html"
    },
    "content_scripts": [
        {
            "matches": [
                "https://chatgpt.com/c/671d0d3e-3bcc-800c-916b-88e0b9831d56",
                "<all_urls>"
            ],
            "js": [
                "content.js"
            ]
        }
    ],
    "background": {
        "service_worker": "background.js"
        }
    }
    ```
    content.jsの中身
    ```js
    // 画像読み込み用の関数
    function addImage() {
        const img = document.createElement('img');
        img.id = 'custom-image';
        img.src = 'https://raw.githubusercontent.com/camen89/chatGPT_extension/dad9ecbfd13cb423ce8083cb75d700a35292d204/%E6%B0%B4%E7%94%BB%E5%83%8F_%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9C%E3%83%BC%E3%83%89%201%20%E3%81%AE%E3%82%B3%E3%83%94%E3%83%BC%203.svg';
        img.style.position = 'fixed';
        img.style.top = '100px';
        img.style.right = `${10 + document.querySelectorAll('.custom-image').length * 40}px`; // 画像を横にずらして追加
        img.style.zIndex = '9999';
        img.style.width = '40px'; // サイズを指定
        img.style.height = 'auto';
        img.className = 'custom-image'; // クラス名を追加して後で参照できるようにする
        // 画像をドキュメントに追加
        document.body.appendChild(img);
    }
    
    //英語のワードカウント機能
    //chat GPTの回答は表示される要素のセレクタ
    const targetSelector = 'div[class="group/conversation-turn relative flex w-full min-w-0 flex-col agent-turn"]';
    
    let lastWordCount = 0;
    let previousSum = 0;
    
    // ml表示用の要素を作成
    const counterDisplay = document.createElement('div');
    counterDisplay.id = 'word-counter';
    counterDisplay.style.position = 'fixed';
    counterDisplay.style.bottom = '10px';
    counterDisplay.style.right = '10px';
    counterDisplay.style.fontSize = '50px';
    counterDisplay.style.backgroundColor = 'rgb(33,33,33)';
    counterDisplay.style.color = 'white';
    counterDisplay.style.padding = '10px';
    counterDisplay.style.zIndex = '10000000';
    document.body.appendChild(counterDisplay); // ページに要素を追加
    
    // 文字数表示用の要素を作成
    const counterMlDisplay = document.createElement('div');
    counterMlDisplay.id = 'ml-counter';
    counterMlDisplay.style.position = 'fixed';
    counterMlDisplay.style.top = '10px';
    counterMlDisplay.style.left = '10px';
    counterMlDisplay.style.fontSize = '30px';
    counterMlDisplay.style.backgroundColor = 'rgb(23,23,23)';
    counterMlDisplay.style.color = 'white';
    counterMlDisplay.style.padding = '10px';
    counterMlDisplay.style.zIndex = '10000000';
    document.body.appendChild(counterMlDisplay); // ページに要素を追加
    
    // 文字数をカウントして画面に表示する関数
    function updateWordCount() {
        // const targetNode = document.querySelector(targetSelector);
        const targetNode = document.querySelectorAll(targetSelector);
        const targetNodeLength = document.querySelectorAll(targetSelector).length;
        console.log(targetNodeLength);
        const targetNodeItem = targetNode.item(`${targetNodeLength}` - 1);
        const targetNodeItemText = targetNode.item(`${targetNodeLength}` - 1).textContent;
        console.log(targetNodeItemText);
    
    
        if (targetNode) {
            const answerText = targetNodeItem.innerText || "";
            const words = answerText.trim().split(/\s+/); // スペースで分割
            const wordCount = words.filter(word => word.length > 0).length; // 空の文字列を除外してカウント
    
            // ワード数が変更された場合のみ処理
            if (wordCount !== lastWordCount) {
                lastWordCount = wordCount;
                console.log(`現在のワード数: ${wordCount}`); // コンソールに表示
    
                // counterDisplay.style.fontSize = `${wordCount}` / 5 + 'px';
    
                // バックグラウンドスクリプトにワード数を送信
                chrome.runtime.sendMessage({ type: "updateCount", count: wordCount });
    
                // ワード数を画面に表示
                counterDisplay.textContent = `${wordCount}` * 519 / 100 + 'ml';
                counterMlDisplay.textContent = `${wordCount} words`;
            }
    
            let sum = 0;
            for (let i = 0; i < targetNodeLength; i++) {
                const targetNodeItem = targetNode.item(i);
                const answerText = targetNodeItem.innerText || "";
                const words = answerText.trim().split(/\s+/);
                const wordCount = words.filter(word => word.length > 0).length;
                console.log(`テキスト${i}:${wordCount}words`);
                sum = sum + wordCount;
                console.log(sum);
            }
            console.log(sum);
            counterMl = `${sum}` * 519 / 100;
            console.log(counterMl);
            counterDisplay.textContent = `${sum}` * 519 / 100 + 'ml';
    
            // 画像を追加
            const imagesToDisplay = Math.floor(counterMl / 500); // 500ごとの章の数
            const existingImages = document.querySelectorAll('.custom-image');
    
            // すでに表示されている画像を削除
            existingImages.forEach(img => img.remove());
    
            // 画像を指定した数だけ追加
            for (let j = 0; j < imagesToDisplay; j++) {
                addImage(10 + j * 60); // 画像を横にずらして追加
            }
        }
    }
    
    // 1秒ごとにワード数を更新
    setInterval(updateWordCount, 1000); // 更新頻度を1秒に変更



    ```
    今回は
    ```js
    img.src = 'https://raw.githubusercontent.com/camen89/chatGPT_extension/dad9ecbfd13cb423ce8083cb75d700a35292d204/%E6%B0%B4%E7%94%BB%E5%83%8F_%E3%82%A2%E3%83%BC%E3%83%88%E3%83%9C%E3%83%BC%E3%83%89%201%20%E3%81%AE%E3%82%B3%E3%83%94%E3%83%BC%203.svg';
    ```
    で画像を読み込んでいて、srcとしてgithubにアップした画像のアドレスを指定しているが、jsと同じ階層において直接ファイル名を指定することも可。

    <br>
    以下はポップアップ用で作っていたコードで結局使わなかったけど一応。
    
    backgroung.jsの中身
    (content.jsで取得した情報を送る的な？あまりよくわかってない、、)
    ```js
    let currentCharacterCount = 0;

    // コンテンツスクリプトからメッセージを受け取り文字数を更新
    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
        if (message.type === "updateCount") {
            currentCharacterCount = message.count;
    
            // コンソールに現在の文字数を表示
            console.log("受信した文字数:", currentCharacterCount);
            
            // ポップアップが開いている場合、リアルタイムで更新
            chrome.runtime.sendMessage({ type: "countUpdated", count: currentCharacterCount });
        }
    });
    
    // ポップアップが開かれたときに現在の文字数を提供
    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
        if (message.type === "getCount") {
            sendResponse({ count: currentCharacterCount });
        }
    });

    ```
    popup.htmlの中身
    ```html
    <!DOCTYPE html>
    <html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>ChatGPT文字数カウンター</title>
        <!-- <style>
            body { font-family: Arial, sans-serif; text-align: center; }
            #count { font-size: 2em; color: #333; }
        </style> -->
        <link rel="stylesheet" href="popup.css">
    </head>
    <body>
        <h1>現在の文字数</h1>
        <div id="count">0</div>
        <p>現在の水消費量</p>
        <div id="count">0<p>ml</p></div> 
        <!-- <img src="楕円のくみあわせ.png" alt=""> -->
        <script src="popup.js"></script>
    </body>
    </html>
    ```
    popup.cssの中身
    ```css
    @charset "utf-8";

    body {
        font-family: Arial, sans-serif;
        text-align: center;
        background-color: black;
    }
    
    h1 {
        color: white;
    }
    
    #count {
        font-size: 2em;
        color: white;
    }
    
    p{
        color: white;
    }
    ```
    popup.jsの中身
    ```js
    // バックグラウンドから最新の文字数を取得して表示
    function updateCount() {
        chrome.runtime.sendMessage({ type: "getCount" }, (response) => {
            document.getElementById("count").textContent = response.count;
        });
    }
    
    // リアルタイムで文字数が更新されたときに表示を更新
    chrome.runtime.onMessage.addListener((message) => {
        if (message.type === "countUpdated") {
            document.getElementById("count").textContent = message.count;
        }
    });
    
    // ポップアップが開かれたときに文字数を初期化
    document.addEventListener("DOMContentLoaded", updateCount);
    ```
    <br>
- **拡張機能の読み込み方**
    manage extensions -> load unpacked -> 上に示したファイルを一つのフォルダに格納したものをローカルから読み込む