簡単にAIを組み込んだサイトが作れます

# 概要

Markdown AIを使っておしゃべりサポーターを作ってみました。
単語を入れると、気の利いた話題3つと、AIによって生成された画像が表示されます。
話題で盛り上がるもよし、画像で盛り上がるもよしのおしゃべりサポーターです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/392df211-e043-f073-9c32-f1df0bc07a36.png)

https://mdown.ai/content/7e73befd-5c53-4612-a8d9-92009ccb2df0

## 感想

大変お手軽でよかったです。こういう簡単にアイデアを形にできるツールと出会った時、結構頭を捻るんですが平凡なものしか出てこなくてサッと面白い発想ができる人間になりたいものです :thinking:

もっと非エンジニアでも使えるようなものに今後なっていくのだろうなと 今からワクワクしてます。

## Markdown AIでできること

Markdown記法でWebサイトの見た目が記載できます。
AIモデルを簡単に作成、サイトに組み込めます。
サイトの公開をボタンポチポチポチで行えます。

### どのように作ったのか?

まず、Create Modelからお題を提供してくれるAIモデルを作ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/8f5f62f4-3fe7-72a1-96e4-7a733003e1e1.png)

#### お題を提供してくれるモデルの作成

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/086d5b95-4888-acbc-52e8-37286260c5f6.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/fba51af6-a7ad-1fa9-3c08-e7998133e2b1.png)

Model NameとPromptを入力します。
Knowledgeは試しに入力してみたりしたんですが何が変わったか分からず、下記見てもよく分からなかったので一旦空欄にしました。

https://qiita.com/mdown_ai_jpn/items/d3e281565c876a0bd64f#%E3%82%B9%E3%83%86%E3%83%83%E3%83%974%E3%83%8A%E3%83%AC%E3%83%83%E3%82%B8%E3%82%92%E8%A8%AD%E5%AE%9A

Createできたら、Playgroundが活性になります。Playgroundでは作ったAIモデルのお試しが可能です :ok_woman: 

##### Playground

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/56f7ccb6-ffe1-5d17-aeed-740323523077.png)

こんなかんじでAIモデルのお試しができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/04cd4508-7ef6-157b-38bf-2969c54e3567.png)

なんかスクロールしてみたら 知らない履歴が出てきた...　他の人のお試し履歴だろうか... :thinking: 

#### 画像生成AIモデルを作る

先ほどはgpt-4o-miniで作成しましたが、今回はdall-e-3で作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/defa8374-3e56-0798-5471-2a940bdfaca0.png)

#### サイトを作る

実際にサイトを作っていきます。UIはこんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/cb29f528-47f7-c597-2ade-978dc4813168.png)

H1でおしゃべりサポーター、H2で単語を入力すると〜と記載します。

```markdown

# おしゃべりサポーター

## 単語を入力すると話題を提供してくれます

```

##### お題を作成してくれるAIモデルの追加

次に、先ほどのお題を作成してくれるAIモデルを入れるためにInsertをクリックします。
Scriptを選択すると、作成したAIモデルが出てくるのでクリックしてInsertします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/6575e1b6-27f8-9b6f-8f64-5fbfdc7b962f.png)

次のコードが追加されますので、見た目部分を書き換えます。

```diff_html
<div style="display: inline-block;">
-  <input class="input" type="text" id="text-1735658349" style="width: 300px;" value="Teach me about Markdown.">
-  <button class="button is-info" type="button" id="button-1735658349">Run AI</button>
+  <input class="input" type="text" id="text-1735653780" style="width: 300px;" value="例: 食べ物、猫">
+  <button class="button is-info" type="button" id="button-1735653780">教えて！</button>
</div>
<div id="answer-1735658349"></div>

<script>
(() => {
  const button = document.getElementById('button-1735658349');
  button.addEventListener('click', async event => {
    button.classList.add('is-loading');
    button.disabled = true;
    
    const serverAi = new ServerAI();
    const message = document.getElementById('text-1735658349').value;
    const answer = await serverAi.getAnswerText('qj57puSTRXh2jfti7hKhiF', '', message);
    
    document.getElementById('answer-1735658349').innerText = answer;
    button.classList.remove('is-loading');
    button.disabled = false;
  });
})();
</script>
```

##### 画像生成AIモデルの追加

Insertをクリックします。Image Scriptを選択すると、作成したAIモデルが出てくるのでクリックしてInsertします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/6e40b87e-0744-e036-5604-5fcaa625cc4e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/46e5b2f4-0fa5-b97a-25b3-4bb0a8a3d19c.png)

次のコードが追加されます。

```html
<div style="display: inline-block;">
  <input class="input" type="text" id="text-1735660769" style="width: 300px;" value="For example, cats." placeholder="Enter a description of the image you want to generate">
  <button class="button is-success" type="button" id="button-1735660769">Run AI</button>
</div>
<div id="image-1735660769"></div>

<script>
(() => {
  const button = document.getElementById('button-1735660769');
  const imageContainer = document.getElementById('image-1735660769');
  let dots = 0;
  let loadingInterval;

  const updateLoadingText = () => {
    dots = (dots + 1) % 4;
    const dotsText = '.'.repeat(dots);
    imageContainer.innerText = `Generating...${dotsText}`;
  };

  button.addEventListener('click', async event => {
    button.classList.add('is-loading');
    button.disabled = true;
    imageContainer.innerText = 'Generating...';
    
    loadingInterval = setInterval(updateLoadingText, 500);
    
    try {
      const serverAi = new ServerAI();
      const prompt = document.getElementById('text-1735660769').value;
      const answer = await serverAi.getAnswer('7ic7nG9Z7vFEZA5THkApzb', {
        prompt: prompt,
        size: '1024x1024',
        n: 1,
        type: 'image'
      });

      clearInterval(loadingInterval);
      imageContainer.innerHTML = `<img src="${answer.imageUrl}" alt="${prompt}">`;
    } catch (error) {
      clearInterval(loadingInterval);
      console.error('An error has occurred.:', error);
      alert(error.message || 'An error has occurred.');
      imageContainer.innerText = 'An error has occurred.';
    } finally {
      button.classList.remove('is-loading');
      button.disabled = false;
    }
  });
})();
</script>
```

ですがこれだと、単語を2回入れなきゃいけなくて面倒なのでちょっと書き換えます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/cbce269d-7db2-e58e-a945-ee3749cf7ee5.png)


#### 最終形

最終的に テキストはこんな感じです。
```html
# おしゃべりサポーター

## 単語を入力すると話題を提供してくれます

<div style="display: inline-block;">
  <input class="input" type="text" id="text-1735653780" style="width: 300px;" value="例: 食べ物、猫">
  <button class="button is-info" type="button" id="button-1735653780">教えて！</button>
</div>
<div id="answer-1735653780"></div>
<div id="image-1735660769" style="width: 512px;"></div>

<script>
(() => {
  const button = document.getElementById('button-1735653780');
  const imageContainer = document.getElementById('image-1735660769');
  let dots = 0;
  let loadingInterval;

  const updateLoadingText = () => {
    dots = (dots + 1) % 4;
    const dotsText = '.'.repeat(dots);
    imageContainer.innerText = `Generating...${dotsText}`;
  };

  button.addEventListener('click', async event => {
    button.classList.add('is-loading');
    button.disabled = true;
    imageContainer.innerText = 'Generating...';
    loadingInterval = setInterval(updateLoadingText, 500);

    const serverAi = new ServerAI();
    try {
      const prompt = document.getElementById('text-1735653780').value;
      const answer = await serverAi.getAnswer('7ic7nG9Z7vFEZA5THkApzb', {
        prompt: prompt,
        size: '512x512',
        n: 1,
        type: 'image'
      });

      clearInterval(loadingInterval);
      imageContainer.innerHTML = `<img src="${answer.imageUrl}" alt="${prompt}">`;
    } catch (error) {
      clearInterval(loadingInterval);
      console.error('An error has occurred.:', error);
      alert(error.message || 'An error has occurred.');
      imageContainer.innerText = 'An error has occurred.';
    }
    const message = document.getElementById('text-1735653780').value;
    const answer = await serverAi.getAnswerText('qj57puSTRXh2jfti7hKhiF', '', message);
    
    document.getElementById('answer-1735653780').innerText = answer;
    button.classList.remove('is-loading');
    button.disabled = false;
  });
})();
</script>
```

Viewボタンを押すと実際の見た目を確認することができます。
動作を確認することも可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/6b06066c-afb7-b96b-965f-42fe7586c67b.png)


### 公開してみる

実際出来上がったコードは次の通りです。
```html
<div class="text-preview"><div><h1 class="is-size-1">おしゃべりサポーター</h1><p style="margin-bottom: 1.35rem"></p><h2 class="is-size-2">単語を入力すると話題を提供してくれます</h2><p style="margin-bottom: 1.35rem"></p><div style="display: inline-block;">
  <input class="input" type="text" id="text-1735653780" style="width: 300px;" value="例: 食べ物、猫">
  <button class="button is-info" type="button" id="button-1735653780">教えて！</button>
</div>
<div id="answer-1735653780"></div>
<div id="image-1735660769" style="width: 512px;"></div>

<script>
(上で貼ったのと同じなので省略)
</script>
</div></div>

<script src="/server-ai-script.js" data-nscript="lazyOnload"></script>
```

server-ai-script.jsもついでに確認しました。
見たとこでふむふむ...ていうだけなんですけどね :smile: 

```js:server-ai-script.js
class ServerAI {
  constructor() {
  }


  getUniqueId(strong = 1000) {
    return new Date().getTime().toString(16) + Math.floor(strong * Math.random()).toString(16)
  }


  /**
   * 
   * @param {*} modelId 
   * @param {*} historyId 
   * @param {*} question 
   * @returns {Promise<string>} text
   */
  async getAnswerText(modelId, historyId, question) {
    const res = await fetch(`/api/chat.get-answer-from-editor`, {
      method: 'POST',
      body: JSON.stringify({
        chatBotId: modelId,
        historyId: historyId,
        question: question,
      }),
      headers: {
        'Accept': 'application/json, */*',
        'Content-type': 'application/json',
        'X-UID': 'hogehoge',
      }
    });

    /**
     * @type {{
     * historyId?: string;
     * text: string;
     * noMemory: boolean;
     * character: 'bot' | 'user'; 
     * }}
     */
    const data = await res.json();
    return data.text;
  }





  /**
   * 
   * @param {string} method 関数名
   * @param {object} parameter 引数 
   */
  // setLocalStorage(method, parameter) {
  //   const newValue = JSON.stringify({
  //     method,
  //     args: parameter
  //   });

  //   localStorage.setItem('caller', newValue);
  //   window.dispatchEvent( new StorageEvent('storage', {
  //     key: 'caller',
  //     newValue: newValue,
  //   }))
  // }

  // getAnswer(modelId, question) {
  //   this.setLocalStorage('getAnswer', {
  //     modelId,
  //     question,
  //   })
  // }

  /**
   * 画像生成用のメソッド
   * @param {string} modelId モデルID
   * @param {object} options オプション
   * @param {string} options.prompt プロンプト
   * @param {string} options.size 画像サイズ
   * @param {number} options.n 生成数
   * @returns {Promise<{imageUrl: string}>}
   */
  async getAnswer(modelId, options) {
    const res = await fetch(`/api/get-image-answer`, {
      method: 'POST',
      body: JSON.stringify({
        modelId: modelId,
        prompt: options.prompt,
        size: options.size,
        n: options.n,
        source: 'playground'
      }),
      headers: {
        'Accept': 'application/json, */*',
        'Content-type': 'application/json',
        'X-UID': 'hogehoge',
      }
    });

    const data = await res.json();
    return data;
  }
}

```

## Markdown AIでできなかったこと

私が最初に作ろうとしたのはWordwolfのサイトでした。

画面遷移が必要なサイトの作成は、今はまだ時期尚早なのかなと感じました。
きっと今後簡単にできるようになるんだと思います。

https://mdown.com/ja/personality-diagnostic-tool/

> 自分1人で最初から最後までコーディングするのは大変なので、ChatGPTにお願いして書き出して貰いました！

今後カバーされるところなのかなと思ってるんですが、いい感じの画面構成作りたい場合は今のところ外付けのAIに頼む感じです
もちろん、MarkdownAIではAIモデル簡単に作れるのでの中に閉じたい場合は作成したAIモデルに頼むのもありですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/54c73341-b419-82bf-195f-01f634eb1671.png)

(...余談...個人的には画面項目作るならClaudeに頼むとプレビューが楽でいい感じです :hugging: )

https://www.anthropic.com/claude


## 最後に

ここまで読んでくださってありがとうございます！
今後もいろんな新しいサービスに触れていきたいです。