Markdown AIで作る利用規約Summarizer

# はじめに
ご覧いただきありがとうございます。みなさんは「利用規約」ってちゃんと読んでますか？

（私は常に全文読んでます。慣れてくると意外とスラスラ読めます。）

Qiitaを読んでいるみなさんは仕事上、もしくは趣味の中で、「利用規約」や「Terms of Service」、「プライバシーポリシー」といった文書を目にする機会が多いかと思います。

本当は読み飛ばしたくないけど、読むのが面倒だから高速スクロールしたりしてませんか？
（私だって読むのは面倒です。）

そこで今回は、利用規約を読むのをサポートしてくれるWebアプリを紹介したいと思います。

# どんなWebアプリ？
Markdown AIを使って開発した、その名も「AI Terms Reader」です。（安直ですね…）

利用規約を丸ごとコピペすると、AIが内容を読み取って要約。さらに特徴を挙げてくれます。

特徴的な場所はリスト化され、項目をクリックすることで該当部分にジャンプでき、自分の目ですぐに確かめられるようになっています。

(リスト機能はベータ版です。たまに全然違う内容のところにジャンプします。)

Webアプリには下のリンクからアクセスできます。

https://mdown.ai/content/d6504f98-37f5-46ea-b1ab-f5f57e3d3f31

![mdown.ai_content_d6504f98-37f5-46ea-b1ab-f5f57e3d3f31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2921137/9bb82013-bcd2-443a-957b-7954212f8a6f.png)

画像はQiitaの利用規約を読ませたものです。

# 開発に使用した技術
HTMLとJavaScriptを使っています。

とはいえJavaScriptを作り込んだのではなく、

+ 画面の操作
+ Markdown AIのサーバーAIとの連携
+ MarkdownをHTMLに変換

といった単純な作業しかさせていません。

リスト化などの複雑な処理はすべてMarkdown AIのサーバーAIにお任せしています。

# Markdown AIのサーバーAIって？
Markdown AIで作成したページに組み込むことができるAIのことです。

2025年1月現在、**GPT-4o mini**・**GPT-3.5 Turbo**・**GPT-4**・**Claude**・**Gemini**の5種類の文章生成AIと、**Dall-E 3**（画像生成AI）を簡単に組み込めるようになっています。

プロンプトを工夫するだけで、面倒な部分のプログラムを書かずにWebアプリを作成できるようになります。

例えば今回紹介しているWebアプリを例に挙げると、

```
あなたは優秀なインターネットユーザーです。今、とあるサービスの利用規約が提示されます。以下の条件に従って回答してください。
1.利用規約のタイトルを読みとり、存在しない場合は想定されるタイトルをMarkdown(#)で出力してください。h1・h2タグが使用可能です。
2.利用規約を読みとり、通常想定される一般的な利用規約と異なる点を抽出し、Markdownを使って分かりやすく日本語で説明してください。
3.最後に、「抽出した箇所の要約」と「抽出した箇所が本文の最初から何文字目にあるか、全角・半角に気を付けながら、空白や改行も含めてカウントしたもの」をペアにし、「<li value="文字数">抽出箇所の要約</li>」と表現して、一覧にして出力してください。ただし、手順「3」はメタデータとして利用するため、追加説明は省いてください。
```

といったプロンプトを**Claude**に投げています。

目的の形で出力してもらうために、もちろん何度かは修正が必要ですが、プログラムのデバックと比べたら非常に簡単だと思います。

各モデルごとに条件をどの程度守るかなどが変わってくるので、上手くいかない場合はそこを変えてみるのも良いと思います。(Claudeはちゃんと条件を守ってくれることが多いのでよく使ってます。)

# ついでに
記事投稿キャンペーンの注意事項に画像生成AIかMermaidを使っていること、という指定があったことに気づいたので、Claudeに指摘してもらった内容が肯定的であるか、否定的であるかを判断し、注意点を風刺画にしてくれる機能を付けてみようと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2921137/4ce44c7f-eacd-416c-baa1-f664c91c182c.png)

AIモデル作成の画面で「dall-e-3」を選択し、

```
あなたは優秀なデザイナーです。今、豊富な知識を持つインターネットユーザーから、ある利用規約についての情報が与えられます。この情報が好意的か、または否定的に受け取れるかを判断し、好意的であれば〇をモチーフに、否定的でならば×をモチーフにし、その情報とともに風刺画をテーマにしてイラストにしてください。
```

とプロンプトを入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2921137/fec15593-1557-4c65-91e0-c8e63081ad67.png)

Markdown AIの編集画面上のInsertボタンをクリックし、「Image Script」ボタンを選んでJavaScriptをごにょごにょしたら…

![mdown.ai_content_d6504f98-37f5-46ea-b1ab-f5f57e3d3f31 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2921137/17168397-bf87-40b2-b20b-625fdb833fc0.png)

できました！

画像は非同期でロードされるので、画像生成の待ち時間はClaudeの説明を読むことができます。

画像生成AIも簡単に組み込めるので、Markdown AI、非常に便利です。

# 作成したコード
今回作成したコードです。MarkdownとHTML、CSS（styleタグ内）、JavaScript（scriptタグ内）が組み合わさっています。

<details open><summary>折りたためます</summary>

```index.md
# AI Terms Reader 
#### 利用上の注意
このサービスはAI(LLM:大規模言語モデル)を利用しています。このサービスは利用規約の解釈をサポートするために作られており、AIによって出力された内容を信頼するかどうかはユーザーに委ねられています。このサービスの開発者、このサイトをホスティングしている株式会社Markdown AI様は、このサービスを利用したことによりユーザーが被った損害、被害等に関して一切の責任を負いません。要約箇所にジャンプする機能はベータ版です。

<div id="container">
  <div id="showAnswer"></div>
  <div id="container2">
    <div id="listPickup"></div>
    <textarea class="input" type="text" id="textArea"></textarea>
  </div>
</div> 

<div class="checkbox-container">
  <input type="checkbox" id="checkBtnAgree" />
  <label for="checkBtnAgree" class="checkBtnLabel">利用上の注意に同意する</label>
  <button class="button is-info" type="button" id="runButton">Run AI</button>
</div>

This page is made by [@kazu_iroiro](https://qiita.com/kazu_iroiro).

<style>
  #container {
    width: 100svw;
  }

  #container2 {
    margin: 0;
    padding: 0;
  }

  /* 初期のスタイル */
  #showAnswer, #listPickup {
    width: 90svw;
    display: none;
    border: 1px solid #000;
    border-radius: 5px;
    padding: 1svw;
    margin: 2.5svw;
    margin-top: 0;
  }

  /* テキストエリアのスタイル */
  #textArea {
    width: 75svw;
    height: 40svh;
    margin: 0 auto;
    padding: 1svw;
    border: 1px solid #000;
    border-radius: 5px;
    margin: 0 0 0 4svw;
  }

  /* チェックボックスとボタンのスタイル */
  .checkbox-container {
    display: flex;
    align-items: center;
    margin: 15px 4svw;
    justify-content: flex-end;
  }
  #checkBtnAgree {
    width: 40px;
    height: 40px;
  }
  .checkBtnLabel {
    margin: 0 10px;
  }

  /* 初期のフェードイン設定 */
  #showAnswer, #listPickup {
    opacity: 0;
    transition: opacity 1s ease-in-out;
  }

  /* 横並びにするためのスタイル */
  @media (min-width: 768px) {
    #container {
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
    }
    #showAnswer {
      width: 40svw;
    }
    #listPickup {
      width: 45svw;
      margin: 0;
    }
    #textArea {
    }
  }
</style>
<script>
(() => {
  const showAnswer = document.getElementById("showAnswer");
  const listPickup = document.getElementById("listPickup");
  const checkbox = document.getElementById("checkBtnAgree");
  const button = document.getElementById("runButton");
  const textArea = document.getElementById("textArea");
  const container2 = document.getElementById("container2");

  button.addEventListener("click", async (event) => {
    if (!checkbox.checked) {
      console.log("Please agree to the terms.");
      return;
    }

    button.classList.add("is-loading");
    button.disabled = true;

    const serverAi = new ServerAI();
    const message = textArea.value;
    const answer = await serverAi.getAnswerText("nzvoYN1YkrMJKeN5vs5smc", "", message);

    console.log(answer);

    // <li>タグが含まれる部分を全て取得
    const listMatches = answer.match(/<li.*?>.*?<\/li>/g) || [];

    // listPartの生成とlistPickupへの反映
    listPickup.innerHTML = `<ul>` + listMatches
      .map(item => {
        const valueMatch = item.match(/value="(\d+)"/);
        const value = valueMatch ? parseInt(valueMatch[1], 10) : 0;
        const text = item.replace(/<li.*?>|<\/li>/g, "").trim();
        return `<li data-value="${value}" class="list-item" style="cursor: pointer; color: blue;">${text}</li>`;
      })
      .join("") + `</ul>`;

    // リストのクリックでカーソル移動
    document.querySelectorAll(".list-item").forEach(item => {
      item.addEventListener("click", (event) => {
        const position = parseInt(event.target.getAttribute("data-value"), 10);
        textArea.focus();
        textArea.setSelectionRange(position, position);
      });
    });

    // showAnswerに変換したテキストを表示
    document.getElementById("showAnswer").innerHTML = markdownToHtml(
      answer.replace(/<li.*?>.*?<\/li>/g, "").trim()
    );

    // showAnswerとlistPickupをフェードイン
    showAnswer.style.display = "block";
    listPickup.style.display = "block";

    var windowWidth = window.innerWidth;
    if (windowWidth >= 768) {
      textArea.style.margin = "0";
      showAnswer.style.margin = "0";
      listPickup.style.margin = "0 0 3svw 0";
      container2.style.margin= "0 10svw 0 0";
      textArea.style.width = "45svw";
      textArea.style.height = showAnswer.offsetHeight - listPickup.offsetHeight - parseFloat(window.getComputedStyle(listPickup).marginBottom) - parseFloat(window.getComputedStyle(textArea).marginTop) -
parseFloat(window.getComputedStyle(textArea).marginBottom) + "px";
    } else {
      textArea.style.width = "90svw";
      textArea.style.margin = "0 2.5svw 2.5svw 2.5svw";
    }

    // フェードインアニメーション
    setTimeout(() => {
      showAnswer.style.opacity = 1;
      listPickup.style.opacity = 1;
    }, 10);

    // 画像生成用の関数を呼び出し
    generateImage(answer);

    button.classList.remove("is-loading");
    button.disabled = false;
  });
})();

function markdownToHtml(markdown) {
    let html = "";
    const lines = markdown.split("\n");

    let inUnorderedList = false;
    let inOrderedList = false;

    lines.forEach((line, index) => {
        // 見出し1 (# ) -> <h1 class="is-size-1">
        if (/^# (.+)/.test(line)) {
            const content = line.match(/^# (.+)/)[1];
            html += `<h1 class="is-size-1">${content}</h1>`;
        } 
        // 見出し2 (## ) -> <h2 class="is-size-2">
        else if (/^## (.+)/.test(line)) {
            const content = line.match(/^## (.+)/)[1];
            html += `<h2 class="is-size-2">${content}</h2>`;
        } 
        // 番号付きリスト (1. 2. で始まる) & **bold** 対応
        else if (/^(\d+)\.\s+(.*)/.test(line)) {
            if (!inOrderedList) {
                html += `<ol>`;
                inOrderedList = true;
            }
            let content = line.match(/^(\d+)\.\s+(.*)/)[2];
            content = content.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>"); // **bold** を変換
            html += `<li>${content}</li>`;
        }
        // 箇条書きリスト ( - や + で始まる)
        else if (/^(\+|-)\s+(.+)/.test(line)) {
            if (!inUnorderedList) {
                html += `<ul>`;
                inUnorderedList = true;
            }
            const content = line.match(/^(\+|-)\s+(.+)/)[2];
            html += `<li>${content}</li>`;
        }
        // 段落 -> <p>
        else if (/^(.+)$/.test(line) && !/^#/.test(line)) {
            if (inUnorderedList) {
                html += `</ul>`;
                inUnorderedList = false;
            }
            if (inOrderedList) {
                html += `</ol>`;
                inOrderedList = false;
            }
            const content = line.match(/^(.+)$/)[1];
            html += `<p>${content}</p>`;
        }
        // 空行
        else if (/^\s*$/.test(line)) {
            if (inUnorderedList) {
                html += `</ul>`;
                inUnorderedList = false;
            }
            if (inOrderedList) {
                html += `</ol>`;
                inOrderedList = false;
            }
            html += `<p style="margin-bottom: 1.35rem"></p>`;
        }
        // 水平線 (---)
        else if (/^---$/.test(line)) {
            html += `<hr class="horizontal-rule">`;
        }

        // 最後の行だった場合、開いているリストを閉じる
        if (index === lines.length - 1) {
            if (inUnorderedList) {
                html += `</ul>`;
            }
            if (inOrderedList) {
                html += `</ol>`;
            }
        }
    });

    return html;
}

function generateImage(promptText) {
  const imageContainer = document.createElement("div");
  imageContainer.id = "image-popup";
  imageContainer.style.position = "fixed";
  imageContainer.style.top = "10%";
  imageContainer.style.left = "50%";
  imageContainer.style.transform = "translate(-50%, -50%)";
  imageContainer.style.backgroundColor = "white";
  imageContainer.style.padding = "20px";
  imageContainer.style.border = "1px solid black";
  imageContainer.style.borderRadius = "5px";
  imageContainer.style.zIndex = "1000";
  imageContainer.style.opacity = "0";
  imageContainer.style.transition = "top 0.5s ease-in-out, opacity 0.5s ease-in-out";
  imageContainer.innerHTML = "<p id='imageMessage'>イメージを生成中</p><div id='image-content'>Generating...</div><button id='close-popup'>閉じる</button>";

  document.body.appendChild(imageContainer);
  setTimeout(() => {
    imageContainer.style.opacity = "1";
  }, 10);
  
  document.getElementById("close-popup").addEventListener("click", () => {
    imageContainer.style.opacity = "0";
    setTimeout(() => {
      document.body.removeChild(imageContainer);
    }, 500);
  });

  (async () => {
    try {
      const serverAi = new ServerAI();
      const answer = await serverAi.getAnswer("w2h74Rrhf9AqxamcoF4t8o", {
        prompt: promptText,
        size: "1024x1024",
        n: 1,
        type: "image"
      });
      document.getElementById("imageMessage").innerText = "画像はイメージです";
      document.getElementById("image-content").innerHTML = `<img src="${answer.imageUrl}" alt="Generated Image">`;
      
      // 画像が埋め込まれたら中央にスムーズに移動
      setTimeout(() => {
        imageContainer.style.top = "50%";
      }, 100);
      
    } catch (error) {
      console.error("An error has occurred:", error);
      document.getElementById("image-content").innerText = "An error has occurred.";
    }
  })();
}
</script>
```

</details>


# おわりに
今回の記事はいかがでしたでしょうか。

Markdown AIを使うと、バックエンドの開発をほとんどせずにサービスを作ることができるのでおススメです。

Webアプリ作ってみたいけど重い腰が上がらない…といった人はぜひこの機会にいかがでしょうか？

少しでも興味を持った方、簡単なので今始めましょう！

下記リンクから公式サイトにアクセスできます。

https://mdown.com/

最後までお読みいただきありがとうございました！

# 参考文献
自分の過去記事です。MarkdownからHTMLへの変換スクリプトをパクってきてます。

https://qiita.com/kazu_iroiro/items/408ab51b6858003d2785

# 余談
とりあえずMarkdown AIの利用規約を読ませてみようかなって思ったらリンク切れてました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2921137/a4169932-8270-4c99-880c-90f353c35b84.png)

以前読んだときは会社名が株式会社トップテン（古い社名？）になっていたので、そのあたりの対応をしているのかもしれませんが、規約が読めないのはどうかと思います。早急に対応をお願いしたいです。