MarkdownAI × QRコードでイベントを盛り上げよう！

# はじめに

**[MarkdownAI](https://mdown.com/ja/)** は、「対話型 AI 」や「画像生成 AI 」を組み込んだ Web ページを簡単に作成できる Web サービスです。

本記事では **Markdown AI** と **QR コード**を組み合わせた活用方法を紹介します。

具体的には、**学園祭などのイベント**での活用例を解説します。
対話型 AI や画像生成 AI は、デバイス内での作業に活用されることが多いですが、QR コードを活用して**現実世界でも利用シーンを広げることが可能**です。

今回作成した Web ページは以下のリンクから見ることができます。デバイスのカメラへのアクセスを有効にしてから利用してください。

https://mdown.ai/content/95dc1463-04ed-4438-8d30-c61ae13bc0a3

<img alt="demo" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2801269/3591337a-262d-17fa-9dfc-1d0a10b65a4b.png" width="300px"/>


# Markdown AI とは？
Markdown AI は、**簡単に** Web ページの**作成から公開まで**を行うことができる Web サービスです。

以下のリンクから **Google アカウント**[^1]のみで**無料**で利用を開始することができます。
[^1]: 日本で Google アカウントを作成するには 13 歳以上である必要があります。

https://mdown.ai/ 

Markdown AI は「**シンプルさ**」と「**自由度の高さ**」が大きな強みで、初心者から Web 開発経験者まで幅広い層のニーズに応えることができるサービスです。

- **シンプルさ**
  - `Markdown` [^2]で手軽に Web ページを作成
  - Web ページの公開は何回かボタンを押すだけ
- **自由度の高さ**
  - `Markdown` だけではなく `HTML`・`CSS`・`JavaScript` も使用が可能
  - さらに `対話型 AI` や `画像生成 AI` の組み込みも可能

Markdown AI は現在 `β 版` であり、機能が続々と追加されています。最新の情報は [News - Markdown AI](https://mdown.com/ja/category/news/) で確認できます。

[^2]: Markdown は**文章の構造を記述するためのルール**です。Web ページの構造は HTML で記述することが一般的ですが、Markdown を使用することで簡単に記述することができます。


# 実装
用意した QR コードを読みとると、学祭のブースの情報が表示され、対話型 AI に質問ができる Web ページを作成します。

学祭のパンフレットや教室の前などに QR コードを載せ、来場者にスマートフォンで読み取ってもらうことを想定しています。


## QR コードの画像を用意
左上から QR コードの内容は `booth1`, `booth2`, `booth3`, `booth4`, `booth5` です。

<img alt="booth1" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2801269/7205ed6c-297f-71a6-da0b-35745c549edd.png" width="150px"/>
<img alt="booth2" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2801269/0a11622f-44c0-81e5-7286-43e82951ecc9.png" width="150px"/>
<img alt="booth3" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2801269/e7667892-08e8-096e-77e3-3fabe1dec9dc.png" width="150px"/>
<img alt="booth4" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2801269/03328f86-945a-7b08-6d9e-c4a5d5ed2469.png" width="150px"/>
<img alt="booth5" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2801269/7e501666-0a7b-30e3-3626-09263e46f955.png" width="150px"/>






## HTML・CSS
必要な HTML と CSS を記述します。
```html
<style>
    *,
    *::before,
    *::after {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }
    html {
        background-color: #08B1D4;
    }
</style>

<div style="margin: 20px 5px;">
<p style="text-align: center; font-size: 2.2rem; font-weight: 600; color: #FFF;">Hoge高 学祭</p>
<div style="position: relative; width: 100%; max-width: 450px; margin: 20px auto;">
    <div id="controls" style="text-align: center; padding: 10px;">
        <button id="toggleVideo" style="width: 300px; height: 60px; background-color: #F82260; color: #FFF; border: 2px solid #FFF; outline: none; border-radius: 5px; font-size: 1.1rem; font-weight: 600;">QR コードを読み取る</button>
    </div>
    <video id="video" autoplay style="display: none; object-fit: fill; width: 100%;  border: 2px solid #FFF; border-radius: 5px;"></video>
    <div id="details" style="display: none; background-color: #FFF; border: 2px solid #bbb; border-radius: 5px; padding: 20px;"></div>
</div>
</div>
<div id="chat-container-qbyaq" style="display: none; margin: 10px auto; max-width: 450px;">
    <div style="max-width: 450px; width: 100%; height: 400px; background-color: #FFF; border: 2px solid #bbb; border-radius: 5px; word-break: break-all;">
        <div id="chat-content-qbyaq" style="overflow-y: auto; height: 78%; margin: 12px 10px 0px 10px; padding: 5px; border-radius: 5px; background-color: #fff;"></div>
        <div style="height: 65px; margin: 5px 10px; display: flex; align-items: center;  justify-content: space-between;">
            <input type="text" id="ai-chat-input-qbyaq" placeholder="質問を入力してください" style="width: calc(100% - 50px); height: 42px; padding: 10px; border: 2px solid #bbb; border-radius: 5px; font-size: 1.05rem;">
            <button id="talk-with-ai-button-qbyaq" type="button" style="width: 45px; height: 40px; border: none; outline: none; border-radius: 5px; user-select: none; background: #363636;">
                <svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-send"><line x1="22" y1="2" x2="11" y2="13" fill="#FFF" stroke="#FFF"/><polygon points="22 2 15 22 11 13 2 9 22 2" stroke="#FFF" /></svg>
            </button>
        </div>
    </div> 
</div>
```
## JavaScript
QR を読み取るためにライブラリとして [`jsQR`](https://www.jsdelivr.com/package/npm/jsqr) を使用するため以下を追記します。
（Markdown AI では現在、CDN を使用した JavaScript の動作をプレビュー画面で確認することができませんが、ページを公開すると機能します。）

```html
<script src="https://cdn.jsdelivr.net/npm/jsqr/dist/jsQR.min.js"></script>
```


次に QR コードを読み取るための JavaScript を追記します。
```javascript
<script>
const video = document.getElementById('video');
const details = document.getElementById('details');
const toggleVideoButton = document.getElementById('toggleVideo');
const chatContent = document.getElementById('chat-content-qbyaq');
const chatbutton = document.getElementById('talk-with-ai-button-qbyaq');
const chatContainer = document.getElementById('chat-container-qbyaq');
let stream = null;
let scanning = false;

const boothData = {
    "booth1": {
        name: "食品コーナー",
        location: "ブース 1",
        description: "地元の食材を使った特製料理をお楽しみいただけます！",
        activity: "焼きそば、たこ焼き、クレープの販売",
        timing: "10:00 - 14:00",
        tags: ["食べ物", "地元食材"]
    },
    "booth2": {
        name: "ゲームブース",
        location: "ブース 2",
        description: "さまざまなミニゲームで遊んで景品をゲット！",
        activity: "射的、輪投げ、くじ引き",
        timing: "9:30 - 15:00",
        tags: ["ゲーム", "景品"]
    },
    "booth3": {
        name: "アート展示",
        location: "ブース 3",
        description: "学生たちの創造性あふれる作品をご覧いただけます。",
        activity: "絵画、写真、立体作品の展示",
        timing: "終日展示",
        tags: ["展示", "アート"]
    },
    "booth4": {
        name: "ステージイベント",
        location: "ブース 4",
        description: "吹奏楽部の演奏、ダンス部のパフォーマンス、そして特別ゲストのライブショーなど、見逃せないイベントが盛りだくさん！",
        activity: "演奏、ダンス、ライブショー",
        timing: "11:00 - 16:00",
        tags: ["イベント", "ライブ"]
    },
    "booth5": {
        name: "写真撮影コーナー",
        location: "ブース 5",
        description: "学祭の思い出を写真に残しませんか？フォトブースで特別な装飾とともに写真を撮影できます。",
        activity: "フォトブース",
        timing: "9:30 - 15:30",
        tags: ["写真", "思い出"]
    }
};


function startVideo() {
    navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } })
    .then((videoStream) => {
        stream = videoStream;
        video.srcObject = stream;
        video.setAttribute("playsinline", true);
        video.style.display = "block";
        video.play();
        scanning = true;
        requestAnimationFrame(scanQRCode);
    });
}

function stopVideo() {
    if (stream) {
        stream.getTracks().forEach(track => track.stop());
        stream = null;
        scanning = false;
        video.style.display = "none";
    }
}

function scanQRCode() {
    if (scanning && video.readyState === video.HAVE_ENOUGH_DATA) {
        const canvas = document.createElement('canvas');
        const context = canvas.getContext('2d');
        canvas.width = video.videoWidth;
        canvas.height = video.videoHeight;
        context.drawImage(video, 0, 0, canvas.width, canvas.height);
    
        const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
        const code = jsQR(imageData.data, canvas.width, canvas.height);
    
        if (code) {
            const boothInfo = boothData[code.data];
            if (boothInfo) {
                details.style.display = "block";
                details.innerHTML = `
                <h3>${boothInfo.name}</h3>
                <p>${boothInfo.description}</p><br/>
                <p><strong>場所:</strong> ${boothInfo.location}</p>
                <p><strong>内容:</strong> ${boothInfo.activity}</p>
                <p><strong>時間:</strong> ${boothInfo.timing}</p>
                <p><strong>タグ:</strong> ${boothInfo.tags.join(" ")}</p>
              `;
                chatContainer.style.display = 'block';
                stopVideo();
                toggleVideoButton.textContent = "QR コードを読み取る";
            } else {
                details.style.display = "none";
            }
        } else {
            details.style.display = "none";
        }
    }
    if (scanning) requestAnimationFrame(scanQRCode);
}

if (!toggleVideoButton.hasAttribute('data-listener')) {
    toggleVideoButton.addEventListener('click', () => {
        if (stream) {
            stopVideo();
            toggleVideoButton.textContent = "QR コードを読み取る";
        } else {
            startVideo();
            toggleVideoButton.textContent = "カメラを停止";
        }
    });
    toggleButton.setAttribute('data-listener', 'true');
}

// 初期状態はビデオオフ
stopVideo();

</script>
```

## 対話型 AI の組み込み　
Markdown AI では Web ページに対話型 AI の追加することができます。

`AI` を作成する場合は、`ロボット`のボタンを押し、Create Model のページに移動します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2801269/b98964c4-03b2-37a6-bb94-f9194419c6db.png" width="250">

そして、`AI` のモデルを作成します。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2801269/0735fe7a-ea71-0325-88fa-27bc68deaf6d.png" width="400">


今回は **Select Model** は無料で使用できる `gpt-4o-mini` にし、**Model Name** は `AI Chat Bot` とします。また、**Prompt** はたとえば以下のような記述をします。
```
学祭ナビゲーションAI プロンプト

あなたは高校の学祭ナビゲーションAIです。来場者に親しみやすく、簡潔かつ丁寧に学祭の案内を行います。以下の内容を元に、訪問者が各ブースやイベントを楽しめるよう最適な情報を提供してください。  

ブース情報
ブース1
・ タイトル: 食品コーナー
・ 場所: ブース 1
・ 説明: 地元の食材を使った特製料理をお楽しみいただけます！
・ 内容: 焼きそば、たこ焼き、クレープの販売
・ 時間: 10:00 - 14:00
・ タグ: 食べ物, 地元食材

ブース2
・ タイトル: ゲームブース
・ 場所: ブース 2
・ 説明: さまざまなミニゲームで遊んで景品をゲット！
・ 内容: 射的、輪投げ、くじ引き
・ 時間: 9:30 - 15:00
・ タグ: ゲーム, 景品

ブース3
・ タイトル: アート展示
・ 場所: ブース 3
・ 説明: 学生たちの創造性あふれる作品をご覧いただけます。
・ 内容: 絵画、写真、立体作品の展示
・ 時間: 終日展示
・ タグ: 展示, アート

ブース4
・ タイトル: ステージイベント
・ 場所: ブース 4
・ 説明: 吹奏楽部の演奏、ダンス部のパフォーマンス、そして特別ゲストのライブショーなど、見逃せないイベントが盛りだくさん！
・ 内容: 演奏、ダンス、ライブショー
・ 時間: 11:00 - 16:00
・ タグ: イベント, ライブ

ブース5
・ タイトル: 写真撮影コーナー
・ 場所: ブース 5
・ 説明: 学祭の思い出を写真に残しませんか？フォトブースで特別な装飾とともに写真を撮影できます。
・ 内容: フォトブース
・ 時間: 9:30 - 15:30
・ タグ: 写真, 思い出

プロンプトの指示
QRコードをスキャンした訪問者には、該当ブースの詳細情報を提供してください。  
質問された場合、適切なブースや次のおすすめを案内します。  例: 「何か食べたい」と言われた場合、食品コーナーを案内します。「次はどこがおすすめ？」と聞かれた場合、ステージイベントや写真撮影コーナーなど適切な提案を行います。  
各ブースの営業時間や場所を考慮し、訪問者が効率的に回れるようにルートを提案してください。  
親しみやすい口調で、来場者の不安を取り除き、楽しい体験をサポートしてください。  
ブース情報に関係のない質問があった場合は、次のように対応してください。丁寧にお断り: 「ブースの情報に関する質問にお答えしています。もしそれに関連することであれば、ぜひ教えてください！」
```

設定が終わったら、`Create` ボタン（もしくは、`Update` ボタン）をクリックします。

そして、編集画面（`https://mdown.ai/file`）に戻ります。

編集画面に戻ったら `Insert` ボタンを押し、

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2801269/d86af0be-c4da-c0bc-f148-d78465d74606.png" width="250">

`Script` ボタンを押します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2801269/afdb6c91-b1be-0d00-7ab9-b1f9d66a7815.png" width="500">


`AI Chat Bot` を選択した状態で `Insert` ボタンを押すと、対話型 AI を Web ページに組み込むための最小限のコードが挿入されます。

`const answer = await serverAi.getAnswerText('モデルのID', '', message);` 内に書かれた `モデルのID` だけをメモして、挿入されたコードをすべて消してください。

`script` タグ内に以下を追記し、以下のコードの`モデルのID` を先ほどメモした ID に書きかえてください。
```javascript
chatbutton.addEventListener('click', async event => {
    chatbutton.disabled = true; 
    chatbutton.style.opacity = '0.3';
    const input = document.getElementById('ai-chat-input-qbyaq');
    let message = input.value.trim();
    if (!message) {
        chatbutton.disabled = false;
        chatbutton.style.opacity = '1';
        return;
    }
    addMessage(message, 'end', '8px 0px 8px 15px', '#f0f0f0', chatContent)
    input.value = ''; 

    chatContent.scrollTop = chatContent.scrollHeight;

    const serverAi = new ServerAI();
    const answer = await serverAi.getAnswerText('モデルのID', '', message);                            
    addMessage(answer, 'start', '8px 15px 8px 0px', '#e6f7ff', chatContent)

    chatContent.scrollTop = chatContent.scrollHeight;
    chatbutton.disabled = false;
    chatbutton.style.opacity ='1';
});
function addMessage(message, justifyContent, margin, backgroundColor ,chatContent){
    const messageElement = document.createElement('div');
    messageElement.style.textAlign = 'left';
    messageElement.style.margin = margin;
    messageElement.style.color = 'black';
    messageElement.style.display = 'flex';
    messageElement.style.justifyContent = justifyContent;
    
    const span = document.createElement('span');
    span.style.display = 'inline-block';
    span.style.backgroundColor = backgroundColor;
    span.style.userSelect = 'text';
    span.style.padding = '8px 12px';
    span.style.borderRadius = '10px';
    span.innerHTML = message;
    messageElement.appendChild(span);

    chatContent.appendChild(messageElement);
}
```

# さらなるアイデア
今回はQR コードと対話型 AI を組み合わせたシンプルな Web ページを作成しました、

ただ、ブースの情報を表示したり、AI に質問できたりするだけでなく、以下のような機能を追加すると魅力的になるかもしれません。

1. **リアルタイムフィードバック**
   - 「どのブースが空いているの？」や「クレープの在庫はある？」といったリアルタイムな質問に対応可能。
   - 主催者からの更新情報を AI が利用し、最新情報を提供。
2. **スタンプラリー機能**
   - 各ブースを訪れるごとにスタンプを集める仕組みを追加。（`localStorage` を利用）
   - 訪問履歴を元に、残りのブースを AI が提案。
3. **AIによる「学祭クイズチャレンジ」**
   - 各ブースで学祭に関連するクイズを出題。正解すると特典が表示される。

また、AI が Markdown 形式で回答してくる場合があるので、`Marked.js` などのライブラリを使用して、見やすく表示する工夫も重要です。`Marked.js` については以下の記事で解説しています。

https://qiita.com/shikuno_dev/items/9d397e5bb435cfb6e5ed