Markdown AIでラジオ番組でお悩み相談できるWebサイトを作ってみた！

# はじめに

ラジオ番組でお悩みを相談できる[Webサイト](https://mdown.ai/content/7028eac8-bb1e-4e7a-b899-c8bdb9e0ec5e)を作りました！

![Screenshot 2025-02-07 at 17.28.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527661/9dbf9c4c-a4df-8f3b-29a0-82a95e78fda4.png)

# サイトでできること
- お悩みを入力
- ラジオ番組の出演者を入力
- ラジオ番組風の会話で、投稿したお悩みに答えてもらう
![Screenshot 2025-02-07 at 17.29.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/527661/0096d453-6a9c-8034-4e81-75d4c04136ba.png)


# 仕組み
### 使用サービス
今回、サイトの作成に [Markdown AI](https://mdown.com/ja/) というサービスを使用しました。
Markdown AI は AI を組み込んだウェブサイトが簡単に作成できるサービスです。
Webサイトのデプロイや AI を使用するトークンを含めて全て無料で使用できます。
Markdown 記法や HTML, CSS, JavaScript を使用してWebサイトを作ることができます。

### プロンプト
ラジオ番組での会話はAIによって自動生成されています。LLMはClaudeを使用しています。
以下はWebサイトで会話を生成するために使用しているプロンプトの例です。
```
あなたは${character}です。
今、ラジオ番組でリスナーさんからの悩み相談に対して${subCharacter}と会話をしています。
以下はお悩みと会話の履歴です。
今の会話に対して${character}らしく${humor}${sentenceNum + 1}文で返答してください。
ただし、会話は${conversationNumber}回行われます。今は${i + 1}回目の会話です。
${conversationNumber}回に収まるように話を収束させてください。
注意：出力はあなたの言葉のみにしてください。会話が何回目かについては言及しないでください。

お悩み:
${concern}

会話の履歴:
${conversationHistory}
```


<details><summary>コード全体</summary>

```html
<style>
    html,
    body {
        height: 100%;
        font-family: 'Arial', sans-serif;
        background-color: #45496a;
        background-image: url("https://cdn.pixabay.com/photo/2017/02/12/12/42/wall-2059909_640.png");
        background-size: cover;
        background-attachment: fixed;
        background-position: center;
        color: whitesmoke;
        padding: 20px;
        text-align: center;
    }

    .neonText {
        color: #fff;
        text-shadow:
            0 0 7px #fff,
            0 0 10px #fff,
            0 0 21px #fff,
            0 0 42px #FFEB00,
            0 0 82px #FFEB00,
            0 0 92px #FFEB00,
            0 0 102px #FFEB00,
            0 0 151px #FFEB00;
    }

    h1 {
        font-size: 6rem;
        animation: pulsate 0.11s ease-in-out infinite alternate;
        margin-bottom: 20px;
    }

    @media (max-width: 768px) {
        h1 {
            font-size: 2rem;
        }
    }

    @keyframes pulsate {

        100% {

            text-shadow:
                0 0 4px #fff,
                0 0 11px #fff,
                0 0 19px #fff,
                0 0 40px #FFEB00,
                0 0 80px #FFEB00,
                0 0 90px #FFEB00,
                0 0 100px #FFEB00,
                0 0 150px #FFEB00;

        }

        0% {

            text-shadow:
                0 0 4px #fff,
                0 0 10px #fff,
                0 0 18px #fff,
                0 0 38px #FFEB00,
                0 0 73px #FFEB00,
                0 0 80px #FFEB00,
                0 0 94px #FFEB00,
                0 0 140px #FFEB00;

        }
    }

    #concern-input {
        max-width: 500px;
        width: calc(100% - 20px);
        padding: 10px;
        resize: vertical;
        font-size: 16px;
        border: 1px solid #45496a;
        border-radius: 5px;
        background-color: #7d8bae;
        color: whitesmoke;
    }

    .input-placeholder::placeholder {
        color: whitesmoke;
        font-size: 12px;
    }

    #chat-box {
        width: 100%;
        max-width: 500px;
        margin: 20px auto;
        padding: 15px;
        background-color: #45496a;
        border-radius: 10px;
        box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
        min-height: 200px;
        overflow-y: auto;
        text-align: left;
    }

    #chat-box p {
        padding: 8px 12px;
        border-radius: 8px;
        margin: 5px 0;
        word-wrap: break-word;
    }

    #concern {
        background-color: #f6efa3;
        color: #7d8bae;
        font-weight: bold;
    }

    #chat-character-name {
        font-weight: bold;
        color: whitesmoke;
    }

    #chat-message {
        background-color: #7d8bae;
        color: whitesmoke;
        font-weight: bold;
    }

    input {
        width: calc(100% - 100px);
        max-width: 400px;
        padding: 12px;
        border: 2px solid #45496a;
        border-radius: 8px;
        font-size: 16px;
        outline: none;
        margin-top: 10px;
        background-color: #7d8bae;
        color: whitesmoke;
    }

    button {
        padding: 12px 20px;
        background-color: #45496a;
        color: #f6efa3;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 24px;
        margin: 10px;
        transition: background-color 0.3s ease;
    }

    button:hover {
        background-color: #213555;
    }

    #next-button-container {
        display: flex;
        justify-content: center;
        margin: 15px 0;
    }

    #next-button {
        background-color: #45496a;
        border: none;
        padding: 10px 15px;
        font-size: 20px;
        border-radius: 50%;
        cursor: pointer;
        transition: background-color 0.3s;
    }

    #next-button:hover {
        background-color: #7d8bae;
    }

    #loading-container {
        display: flex;
        justify-content: center;
        margin: 15px 0;
    }

    .loader {
        width: 40px;
        height: 40px;
        border: 4px solid #f3f3f3;
        border-top: 4px solid #7d8bae;
        border-radius: 50%;
        animation: spin 1s linear infinite;
        margin: auto;
    }

    @keyframes spin {
        0% {
            transform: rotate(0deg);
        }

        100% {
            transform: rotate(360deg);
        }
    }

    #input-container {
        display: flex;
        flex-direction: column;
        align-items: center;
        /* 子要素を中央揃え */
        width: 100%;
    }

    textarea,
    input {
        width: 80%;
        /* 全幅ではなく、適度なサイズに調整 */
        max-width: 400px;
        /* 最大幅を設定 */
        margin-bottom: 10px;
    }

    #select-concern,
    #select-characters {
        padding: 12px 20px;
        background-color: #45496a;
        color: #f3f3f3;
        border: none;
        border-radius: 8px;
        cursor: pointer;
        font-size: 12px;
        margin: 8px;
        transition: background-color 0.3s ease;
    }

    #select-concern:hover,
    #select-characters:hover {
        background-color: #213555;
    }
</style>

<h1 class="neonText">お悩み相談ラジオ</h1>
<div id="chat-box" style="display: none;"></div>
<div id="next-button-container" style="display: none;">
    <button id="next-button">↓</button>
</div>
<div id="loading-container" style="display: none;">
    <div class="loader"></div>
</div>
<div id="input-container">
    <button id="select-concern">お悩みを自動入力する</button>
    <textarea id="concern-input" class="input-placeholder" placeholder="お悩みを入力..." rows="3"></textarea>
    <button id="select-characters">お悩み相談する人を自動入力する</button>
    <input id="character1" type="text" class="input-placeholder" placeholder="ラジオ番組の出演者1を入力...">
    <input id="character2" type="text" class="input-placeholder" placeholder="ラジオ番組の出演者2を入力...">
    <button id="start-button">相談する</button>
</div>
</div>

<script>

    document.getElementById('start-button').addEventListener('click', startConversation);
    document.getElementById('next-button').addEventListener('click', showNextMessage);
    document.getElementById('select-concern').addEventListener('click', selectConcern);
    document.getElementById('select-characters').addEventListener('click', selectCharacters);

    let conversationQueue = [];
    let isLoading = false;

    async function selectConcern() {
        const sampleConcerns = [
            "おでんの具のことでいつも夫と喧嘩になります。夫は絶対にハンペンを入れたがらないのですが、私はハンペンを絶対に入れたいです。どうすれば夫と喧嘩せずにこの冬を乗り切れますか？",
            "飲み会に行くと、周辺の音が気になってしまい、話が聞こえず、会話に参加できません。どうすれば飲み会で楽しめますか？",
            "カフェで注文するとき、毎回「いつもので！」と言うのに、店員さんが「いつもの？」と聞いてくるのが嫌です。どうすれば店員さんに早く覚えてもらえますか？",
            "お弁当の唐揚げを最後に取っておいたら、同僚に「それ食べないんですか？」と言われ、つい譲ってしまいました。どうすれば自分の唐揚げを守れますか？",
            "スマホをいじっていないと手持ち無沙汰なので、ポケットから出し入れするだけの謎の動作を繰り返しています。何かもっとスマートな時間の潰し方はありますか？",
            "電車でお年寄りに席を譲ったら、「まだ若いんで！」って断られました。ダメージを負わずに席を譲る方法はありますか？",
            "会社の同僚が私の真似をし始めました。髪型、服装、お弁当まで...。怖いのですが、直接言えません。どうすればいいですか？",
            "家族全員が私の作る味噌汁を「お母さんの味噌汁は本当に美味しいね」と絶賛するのですが、実は市販のインスタント味噌汁なんです。本当のことが言えません。どうすればいいですか？",
            "マンションの管理人さんが、私のことを「山田さん」だと思い込んでいます。本当は「田中」なのですが、訂正するタイミングを逃してしまいました。どうすればいいですか？",
            "部下が私の「頑張れ」という言葉を録音して着信音にしています。恥ずかしいのですが、言い出せません。どうすればいいですか？",
        ];
        const randomIndex = Math.floor(Math.random() * sampleConcerns.length);
        document.getElementById('concern-input').value = sampleConcerns[randomIndex];
    }

    async function selectCharacters() {

        const sampleCharacters = ["ミッキーマウス", "ドナルド・トランプ", "スティーブ・ジョブズ", "孔子", "イーロン・マスク", "ビル・ゲイツ", "織田信長", "アインシュタイン", "クレオパトラ", "聖徳太子"];

        const randomIndex1 = Math.floor(Math.random() * sampleCharacters.length);
        document.getElementById('character1').value = sampleCharacters[randomIndex1];
        let randomIndex2 = Math.floor(Math.random() * sampleCharacters.length);
        while (randomIndex2 === randomIndex1) {
            randomIndex2 = Math.floor(Math.random() * sampleCharacters.length);
        }
        document.getElementById('character2').value = sampleCharacters[randomIndex2];
    }

    async function startConversation() {
        document.getElementById('chat-box').style.display = "block";
        isLoading = true;

        let conversationHistory = "";

        const input = document.getElementById('concern-input');
        const chatBox = document.getElementById('chat-box');
        const nextButton = document.getElementById('next-button-container');
        const loadingContainer = document.getElementById('loading-container');
        const concern = input.value.trim();
        const character1 = document.getElementById('character1').value.trim();
        const character2 = document.getElementById('character2').value.trim();

        const conversationNumber = 6;

        if (!concern) {
            alert("お悩みを入力してください");
            return;
        }
        if (!character1 || !character2) {
            alert("ラジオ番組の出演者を入力してください");
            return;
        }
        loadingContainer.style.display = "block";

        chatBox.innerHTML = `<p id="concern">お悩み:<br>${concern}</p>`;
        input.value = '';
        conversationQueue = [];

        removeInput();
        const serverAi = new ServerAI();
        const firstMessage = `あなたは${character1}です。${character2}とラジオ番組をやっています。リスナーさんからの悩み相談が届きました。まずは番組名コールをし挨拶から始めて、お悩みを紹介するだけにしてください。注意：出力はあなたの言葉のみにしてください。\nお悩み:\n${concern}`;
        const response = await serverAi.getAnswerText('7M1Cgeu4c9xhSyGwfYMCux', '', firstMessage);
        conversationQueue.push({ character: character1, message: response });
        conversationHistory += `${character1}: ${response}`;

        showNextMessage();

        for (let i = 0; i < conversationNumber; i++) {
            scrollToBottom();
            const character = i % 2 === 0 ? character2 : character1;
            const subCharacter = i % 2 === 0 ? character1 : character2;
            const randomValue = Math.random()
            const sentenceNum = Math.floor(Math.random() / 3 * 10);
            const humor = randomValue > 0.7 ? "ユーモアを持って" : "";
            let prompt = "";
            if (i === conversationNumber - 1) {
                prompt = `あなたは${character}です。今、ラジオ番組でリスナーさんからの悩み相談に対して${subCharacter}と会話をしています。以下はお悩みと会話の履歴です。今の会話に対して返事をしつつ、ラジオの最後の締めくくりをしてください。注意：出力はあなたの言葉のみにしてください。\nお悩み:\n${concern}\n会話の履歴:\n${conversationHistory}\n${character}:\n`;
            } else {

                prompt = `あなたは${character}です。今、ラジオ番組でリスナーさんからの悩み相談に対して${subCharacter}と会話をしています。以下はお悩みと会話の履歴です。今の会話に対して${character}らしく${humor}${sentenceNum + 1}文で返答してください。ただし、会話は${conversationNumber}回行われます。今は${i + 1}回目の会話です。${conversationNumber}回に収まるように話を収束させてください。注意：出力はあなたの言葉のみにしてください。会話が何回目かについては言及しないでください。\nお悩み:\n${concern}\n会話の履歴:\n${conversationHistory}\n${character}:\n`;
            }
            const response = await serverAi.getAnswerText('7M1Cgeu4c9xhSyGwfYMCux', '', prompt);
            const replacedResponse = response.replace(`${character}: `, "").replace(/(^「|」$)/g, "");
            conversationHistory += `\n${character}: ${replacedResponse}\n`;
            conversationQueue.push({ character, message: replacedResponse });
            nextButton.style.display = "block";
            loadingContainer.style.display = "none";
        }
        isLoading = false;
    }

    function removeInput() {
        document.getElementById('input-container').style.display = "none";
    }

    function scrollToBottom() {
        setTimeout(() => {
            window.scrollTo({
                top: document.body.scrollHeight,
                behavior: 'smooth'
            });
        }, 100); // 100ms 後にスクロール
    }

    function showNextMessage() {
        if (conversationQueue.length) {
            const chatBox = document.getElementById('chat-box');
            const { character, message } = conversationQueue.shift();

            const newMessage = document.createElement('div');
            newMessage.innerHTML = `<p id="chat-character-name">${character}</p><p id="chat-message">${message}</p>`;
            chatBox.appendChild(newMessage);
            scrollToBottom();
        }

        const nextButtonContainer = document.getElementById('next-button-container')

        if (conversationQueue.length) {
            nextButtonContainer.style.display = "block";
        } else {
            nextButtonContainer.style.display = "none";
            if (isLoading) {
                document.getElementById('loading-container').style.display = "block";
            }
        }
    }

</script>
  ```
   </details>

# 工夫したこと
- お悩み相談の入力でユーザーが相談事をすぐに思いつかないこともあるかなと思ったので、自動でサンプル文が入力できるようにしました。また、ラジオの出演者の入力も自由入力と自動入力どちらも用意して入力の障壁を低くしました。
- AI同士の会話が単調になってしまうことがあったので、プロンプトに３０％の確率で「ユーモアを持って」という文言を入れています。

お悩みに対して、なかなか面白い回答をしてくれることがあるので、ぜひ一度使ってみてほしいです😄
Webサイトは[こちら](https://mdown.ai/content/7028eac8-bb1e-4e7a-b899-c8bdb9e0ec5e)です！

# 最後に
Markdown AI を使うと、すごく簡単にAIを組み込んだサイトが作れます。
他にも[相関図クイズゲーム](https://mdown.ai/content/eb995fb2-c2a4-4772-bd17-08c52573f5d1) や [孤独のグルメシミュレーションゲーム](https://mdown.ai/content/2b4eb63b-d7af-4041-91c3-d8db98adba66)を作ったりして非常に楽しく遊ばせていただいています。
とてもおすすめなのでぜひみなさんも Markdown AI を使ってAIのサービス開発してみてください！

https://qiita.com/thekiai/items/ada5d62e145c53f6a7f3