Voiceflow×Airtable×Glide×スプレッドシート（GAS）で勉強記録が残せるWebページを作成

前回、こちらの記事でVoiceflowとChatGPTを繋げて、ポケモンの名前を音声で聞けば特徴答えを返してくれて、それをVoiceflowの返答として登録出来るおもちゃを作りました。

https://qiita.com/hiroaki0724/items/a42f87a8d55c46847605

簡単に説明すると
①　調べたいポケモンの名前を言う
②　①のポケモンがアプリに登録されていれば、登録している解説が流れる
③　①のポケモンがアプリに登録されていなければChatGPTの回答を読み上げる

その後、③で読み上げられた回答について自分で調べて、情報が正しければその情報を声に出して読み上げるとAirtabeleというスプレッドシートのような場所に情報が登録され、その内容をVoiceflowに登録（ここは手動）すれば、そのポケモンの名前を言ったときに、自分で解説した内容が音声で返ってくる。というものです。

これはこれでそれなりに楽しんでくれたのですが、最近嬉しいことにポケモン以外にも色々と興味を持って聞いてきてくれるようになりました。
外を一緒に歩いていても、
・夏はなんで暑いの？
・セミは何で鳴くの？
・アリは何で行列を作るの？
といったように色々と質問してくれます。

その都度、一緒に考えながら答えを探しており、色んな学びや気づきがあると思うのですが、現状それが残せていないのはもったいない。
紙に書き留めていくのは時間がかかりすぎるし、パソコンのキーボードはまだ打てない。
今の年齢だと「話して登録」というのが一番効率よく、楽しく出来ると思います。
なにより、目に見えて溜まっていった方がモチベーションにも繋がるはず

そこを今回は実装してみました。

より詳しい経緯はこちらの記事にまとめておりますので、よろしければお読みください。

https://note.com/hiroaki_koyama/n/n00319fe1cf08

## 全体のフロー

![フロー.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292587/a49bd3dd-ac1b-5aa5-cf36-409e3c51684f.jpeg)

※1部、実際の動きと順番が異なる部分がありますが、分かりやすくするために番号を振っております。

## それぞれの動き

### ①Voiceflowで学んだ内容を登録

詳細は上にも上げたこちらの記事をご参考頂ければと思います。

https://qiita.com/hiroaki0724/items/a42f87a8d55c46847605

作ったものがこちら

https://creator.voiceflow.com/prototype/64c865b8934e9daec47addbb

※PC環境でのみ、動きます。

### ②Airtableからスプレッドシートに転記

この部分はAirtableに機能として付いているAutomationsを使いました。

参考にしたのはこちらの記事

https://note.com/mucho3/n/n3e687c9d100c

画像の左から4番目のカラム（category）の内容をスプレッドシートのA列に転記しています。

・Airtable
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292587/1801c5cf-e720-a2de-1110-8a0b39b98eac.png)

・スプレッドシート
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292587/e6f08baf-ec7c-76a8-f9a9-bef82b4f1bc6.png)

スプレッドシートに転記させるついでに、メール（Gmail）に通知が来る設定も行いました。

分かる方であれば、Voiceflowからスプレッドシートに直接情報を登録すれば、わざわざこんなことをしなくてもいいのに。
と思われるかと思いますが、Voiceflowのスプレッドシート連携ブロックは2022年7月で廃止され、現在はAPIブロックを利用して読み取り専用アクセスしかできなくなっている為、Airtableを間に入れております。

https://learn.voiceflow.com/hc/en-us/articles/10796629279629--Legacy-Google-Sheets-Step


### ③画像生成～⑥格納URL取得

GASを使っております。

・openaiのAPIを叩いて、画像を生成
・プロンプトはスプレッドシートに転記（A列）された内容
・更新されたとき（A列に情報が追加）ときに動くようにトリガーを設定
・生成された画像をGoogleDriveに格納
・格納先のURLを取得し、B列に追記

<details><summary>********** コードはこちら **********</summary>

```js

const GPT_TOKEN = 'sk-●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●●';
const GPT_ENDPOINT = 'https://api.openai.com/v1/images/generations';
const FOLDER_ID = '14emE6sU_●●●●●●●●●●●●●●●●●●●●●●●●●';

function myFunction() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  const promptCellAddress = sheet.getRange("C1").getValue(); // C1セルの値を取得（例: "A7"）
  const prompt = sheet.getRange(promptCellAddress).getValue(); // C1セルに記載されたセルの値（例: A7セルの値）を取得
  const destinationCellAddress = sheet.getRange("D1").getValue(); // D1セルの値を取得（例: "B15"）
  createImage(prompt, destinationCellAddress, sheet); // セルの値をプロンプトとして渡す
}

function createImage(prompt, destinationCellAddress, sheet) {
  const headers = {
    'Authorization': 'Bearer ' + GPT_TOKEN,
    'Content-type': 'application/json',
  };
  // リクエストオプション
  const options = {
    'method': 'POST',
    'headers': headers,
    'payload': JSON.stringify({
      "prompt": prompt,
      "n": 1, // 画像を生成する枚数を設定(1～10)
      "size": "1024x1024"
    })
  };
  // HTTPリクエストでChatGPTのAPIを呼び出す
  const res = JSON.parse(UrlFetchApp.fetch(GPT_ENDPOINT, options).getContentText());
  let image_url = '';

  for (i = 0; i < res['data'].length; i++) {
    image_url = res['data'][i]['url'];

    let blob = UrlFetchApp.fetch(image_url).getBlob();
    let fileBlob = blob.setName(prompt + '_' + i);

    let folder = DriveApp.getFolderById(FOLDER_ID)
    const imageFile = folder.createFile(fileBlob);

    // 保存された画像のURLを指定されたセルに記録する
    const imageUrlCell = sheet.getRange(destinationCellAddress);
    imageUrlCell.setValue(imageFile.getUrl());
  }
}


```
</details>

### ⑦画像データ転記

スプレッドシートに入力されたURLをAirtableのthumbnailカラムに転記

・スプレッドシート
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292587/fdcb03f8-3400-a5bf-dc66-dcefda174706.png)

・Airtable
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292587/7cab9f42-b16a-32b9-7baa-6b905732e362.png)

ここもGASを使って自動でAirtableに転記されるようにしたかったのですが、何度やってもうまくいかず。
URLは取得出来るのですが、thumbnailカラムの一番最初の空白に入力する。というところがうまく動きませんでした。

プロンプト自体も粗いので、狙いとは全く異なった画像が出て来ることも多いので、自分の目で確認してから設定した方が良い（言い訳）と思い、一旦は未実装です。

<details><summary>********** コード（未実装）はこちら **********</summary>

```js

const airtableApiKey = 'key1rzEMHuaMH8NGP';
const baseId = 'app0I6h4OAeukL6iL';
const table = 'study'; // Airtableのテーブル名を指定




function writeToAirtable(url) {
  const apiUrl = `https://api.airtable.com/v0/${baseId}/${table}`;
  const records = getAirtableRecords(apiUrl, airtableApiKey);

  let recordIdToUpdate = null;
  for (const record of records) {
    if (!record.fields.thumbnail) {
      recordIdToUpdate = record.id;
      break;
    }
  }

  if (!recordIdToUpdate) {
    console.log('空白セルのあるレコードが見つかりませんでした。');
    return;
  }

  const data = {
    "fields": {
      "thumbnail": url // AirtableのthumbnailカラムにURLをセット（配列で渡す必要がある場合があります）
    }
  };

  const options = {
    'method': 'PATCH',
    'contentType': 'application/json',
    'headers': {
      'Authorization': 'Bearer ' + airtableApiKey,
    },
    'payload': JSON.stringify(data)
  };

  const updateUrl = `${apiUrl}/${recordIdToUpdate}`;
  const response = UrlFetchApp.fetch(updateUrl, options);
  console.log(response.getContentText());
}

function getAirtableRecords(apiUrl, apiKey) {
  const options = {
    'headers': {
      'Authorization': 'Bearer ' + apiKey,
    },
  };



  const response = UrlFetchApp.fetch(apiUrl, options);
  const data = JSON.parse(response.getContentText());
  return data.records;
}


function onEdit(e) {
  const editedRange = e.range;
  const columnB = 2; // B列のインデックス（0から始まる）
  const lastRow = editedRange.getSheet().getLastRow();

  // B列に編集があった場合、かつ最後の行が編集された場合にAirtableに転記
  if (editedRange.getColumn() === columnB && editedRange.getRow() === lastRow) {
    const url = editedRange.getValue();
    writeToAirtable(url);
  }
}

```

</details>


### ⑧Glideに反映

反映自体は最初の設定さえしてしまえばこちらで行う事はなく、連携しているAirtableと自動で同期されます。
最初の設定はこちらを参考にしました。

https://note.com/shinya_matsui/n/nfe1c01e2c63c


### ⑨Glideに表示

こちらも特に行う事はありません。
左がTOPページ、右が左上の「朝顔」をタップした先に表示される詳細ページです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3292587/06b0ced2-1bcf-a492-06d9-146ab56e954a.png)

実際のページがこちら

https://studynote.glide.page


## 作ってみて思った事

今回、Airtableに入った情報を使って外部（スプレッドシート）と連携させる。
ということや、Glideを使うのも初めての事でしたが、すごく直観的に動かせる部分が多く、初めての動作でも調べずに作ることが一部出来、ノーコードツールのすごさを感じました。

とは言え、ほとんどの部分は調べて実装しましたが、先人の方がたくさん記事を残して下さっていて、本当に助かりました。自分のメモの残し方の雑さに呆れます。。。

雑多すぎるメモ

https://zenn.dev/hiroaki0724/scraps/47f9a94dc0aa91

GASに関しても、ChatGPTを使えば完璧とまではいかないまでもちゃんとコードを返してくれるし、プログラミングを始めた2か月前に比べると、ほんの少しだけ、聞き方もうまくなってきた気がします。

何よりも、これからもどんどん新しいサービスやツールが出て来ると思いますが、恐れずにまずは使ってみる。という事を続けていきたいと思います。