🙄

Gemini で GAS の web アプリをつくる

に公開

【初学者向け】テーブルデータと連携させた web アプリをつくる

この記事は Google Workspace を利用している企業の生成 AI 初学者のためにつくったチュートリアルです。広くサービスを見渡すと、例えば v0 などもっと簡単に Web アプリをつくれるサービスもありますが、今回は先述した法人でも実施しやすくするために Gemini と GAS を使うことにしました。

以下のステップで実装します。

1. データを用意する

スプレッドシートで時系列のデータを用意します。ご自身の実験データを使ってください。
データが無ければ、Gemini でつくっても良いです。

image.png

image.png

2. GAS の web アプリの開発を Gemini で指示する

スプレッドシートの拡張(Extensions)からApp Script を選びます。

image.png

GAS(Google App Script)のウィンドウが開きます。

image.png

コーディングを始めます。Gemini につくって欲しいアプリの内容を伝えましょう。

次のプロンプトを参考にして、ご自身でカスタマイズしてください。

スプレッドシートのデータを使ってGASのアプリをつくってください
# 体重管理・将来予測アプリの作成
スプレッドシートに記録されたユーザーの日付ごとの体重データと将来の体重が予測されるチャートが表示されるアプリをつくってください。

## 必要な機能
1. ユーザー入力:
    - ユーザーの性別
    - ユーザーの身長
    - 連携の実行

2. 計算機能:
    - スプレッドシートに入力された値に基づいて、1か月後の体重を計算する

3. 表示機能:
    - 1か月後の体重を表示
    - 日にちの経過に伴う体重の変化をグラフで表示

## デザイン要件
    - モバイルフレンドリーなレスポンシブデザイン
    - シンプルで使いやすいUI
    - 結果は視覚的に分かりやすく表示

## 技術的な詳細
    - 体重は食事等で一日でプラスマイナス1kg変動することがある

## 言語
    - 日本語のUIで作成

## 開発環境
    - Google App Script でwebアプリとしてデプロイする

image.png

コードを生成してくれます。

image.png

Gemini の Canvas 機能でプレビューすることもできるので、思った通りのアプリになっているか確認しながら適宜修正を入れていきましょう。

image.png

生成したコード
Code.gs
const SHEET_NAME_DATA = 'データシート';
const WEIGHT_FLUCTUATION = 1; // 一日の体重変動幅(kg)

function doGet(e) {
  return HtmlService.createTemplateFromFile('index')
      .evaluate()
      .setTitle('体重管理・将来予測アプリ')
      .setFaviconUrl('https://ssl.gstatic.com/docs/script/images/favicon.ico');
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

function getWeightData() {
  const dataSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_DATA);
  const lastRow = dataSheet.getLastRow();
  if (lastRow < 2) {
    return [];
  }
  const dataRange = dataSheet.getRange(2, 1, lastRow - 1, 2);
  const values = dataRange.getValues();
  return values.map(row => ({ date: Utilities.formatDate(new Date(row[0]), Session.getTimeZone(), 'yyyy-MM-dd'), weight: row[1] }));
}

function predictWeight(gender, height) {
  const weightData = getWeightData();
  if (weightData.length === 0) {
    return { predictedWeight: 'データがありません', chartData: [] };
  }

  const latestWeight = weightData[weightData.length - 1].weight;
  const predictedWeight = latestWeight + (Math.random() * 2 * WEIGHT_FLUCTUATION - WEIGHT_FLUCTUATION); // ランダムな変動を加味

  // 過去30日間のデータと予測データをチャート用に整形
  const chartData = [['Date', 'Weight']];
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - 30);

  const filteredData = weightData.filter(item => new Date(item.date) >= startDate);
  filteredData.forEach(item => {
    chartData.push([item.date, item.weight]);
  });

  // 30日後までの予測データを追加 (簡易的な直線予測)
  const lastDate = new Date(weightData[weightData.length - 1].date);
  for (let i = 1; i <= 30; i++) {
    const nextDate = new Date(lastDate);
    nextDate.setDate(lastDate.getDate() + i);
    const predictedNextWeight = latestWeight + (Math.random() * 2 * WEIGHT_FLUCTUATION - WEIGHT_FLUCTUATION) * (i / 10); // 徐々に変動幅を大きく
    chartData.push([Utilities.formatDate(nextDate, Session.getTimeZone(), 'yyyy-MM-dd'), predictedNextWeight]);
  }

  return { predictedWeight: predictedWeight.toFixed(1), chartData: chartData };
}
index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>体重管理・将来予測</title>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
  <style>
    body {
      font-family: sans-serif;
      padding: 20px;
    }
    .container {
      max-width: 600px;
      margin: 0 auto;
    }
    .form-group {
      margin-bottom: 20px;
    }
    #result {
      margin-top: 30px;
      padding: 20px;
      border: 1px solid #ccc;
      border-radius: 5px;
      background-color: #f9f9f9;
    }
    #chart_div {
      margin-top: 30px;
    }
  </style>
</head>
<body>
  <div class="container">
    <h1>体重管理・将来予測</h1>
    <div class="form-group">
      <label for="gender">性別:</label>
      <input type="text" class="form-control" id="gender">
    </div>
    <div class="form-group">
      <label for="height">身長 (cm):</label>
      <input type="number" class="form-control" id="height">
    </div>
    <button class="btn btn-primary" onclick="runPrediction()">連携を実行</button>

    <div id="result" style="display:none;">
      <h2>予測結果</h2>
      <p>1か月後の体重(予測): <span id="predictedWeight"></span> kg</p>
    </div>

    <div id="chart_div" style="width: 100%; height: 400px; display:none;"></div>
  </div>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
  <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
  <script>
    $(document).ready(function() {
      loadUserInfo();
    });

    function loadUserInfo() {
      google.script.run.getUserInfo().then(function(userInfo) {
        $('#gender').val(userInfo.gender);
        $('#height').val(userInfo.height);
      });
    }

    function runPrediction() {
      google.script.run.predictWeight().then(function(result) {
        $('#predictedWeight').text(result.predictedWeight);
        $('#result').show();
        if (result.chartData && result.chartData.length > 1) {
          drawChart(result.chartData);
          $('#chart_div').show();
        } else {
          $('#chart_div').hide();
        }
      });
    }

    function drawChart(chartData) {
      google.charts.load('current', {'packages':['corechart']});
      google.charts.setOnLoadCallback(function() {
        var data = google.visualization.arrayToDataTable(chartData);

        var options = {
          title: '過去30日と将来30日の体重変化',
          hAxis: { title: '日付' },
          vAxis: { title: '体重 (kg)' },
          legend: { position: 'bottom' },
          curveType: 'function'
        };

        var chart = new google.visualization.LineChart(document.getElementById('chart_div'));
        chart.draw(data, options);
      });
    }
  </script>
</body>
</html>

コードができたら、GAS の方に書いていきましょう。
ちなみに、私のサンプルアプリは、Code.gsindex.html の2つのファイルがあるので、それらを実装します。

Code.gs は GAS を開いた時に初めから用意されているので、それを書き換えます。

image.png

続いて、左のサイドバーから HTML のファイルを追加し、index.html という名前を付けます

image.png

index という名前だけ付ければ拡張子(.html)は自動で付与されます。

同じようにコードを貼り付けます。

image.png

一度プロジェクトを保存しましょう。保存のアイコンかショートカットキーを使います。
保存がされていない時は、ファイルの左側にオレンジ色のマークが出ます。

image.png

3. スプレッドシートと連携させる

最後のステップです。最初につくったスプレッドシートと連携させましょう。
ここから先は生成されたコードによりやることが変わってきます。

この辺りも、Gemini に次のように聞けば教えてくれます。

スプレッドシートの ID など、データを連携させるためにコードに書き換える必要がある個所を教えてください

1. 現在開いているスプレッドシートを取得している

上述した私のサンプルコードはこのパターンです。
この GAS はスプレッドシートのプロジェクトから立ち上げたため、SpreadsheetApp.getActiveSpreadsheet()で取得可能です。

ただし、シート名を指定しているので、スプレッドシートのシート名を変えるか、コードを変えるか、をして揃えて指定するシート名を揃えてください。

image.png

2. ID の指定を指定して取得している

この場合は、SpreadsheetApp.openById()で取得します。ID というのはスプレッドシートに割り振られたユニークな値で、以下の URL のXX...XXの部分になります。

https://docs.google.com/spreadsheets/d/XXXXXXXXXXXXXXXXXXXXXXXXXXX

image.png

サンプルコード
Code.gs
const SHEET_NAME_DATA = 'データシート';
const WEIGHT_FLUCTUATION = 1; // 一日の体重変動幅(kg)
const SPREADSHEET_ID = 'ここにあなたのスプレッドシートIDを入力してください'; // ★ IDをここに入力

function doGet(e) {
  return HtmlService.createTemplateFromFile('index')
      .evaluate()
      .setTitle('体重管理・将来予測アプリ')
      .setFaviconUrl('https://ssl.gstatic.com/docs/script/images/favicon.ico');
}

function include(filename) {
  return HtmlService.createHtmlOutputFromFile(filename).getContent();
}

function getWeightData() {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID); // IDでスプレッドシートを開く
  const dataSheet = ss.getSheetByName(SHEET_NAME_DATA);
  const lastRow = dataSheet.getLastRow();
  if (lastRow < 2) {
    return [];
  }
  const dataRange = dataSheet.getRange(2, 1, lastRow - 1, 2);
  const values = dataRange.getValues();
  return values.map(row => ({ date: Utilities.formatDate(new Date(row[0]), Session.getTimeZone(), 'yyyy-MM-dd'), weight: row[1] }));
}

function predictWeight(gender, height) {
  const weightData = getWeightData();
  if (weightData.length === 0) {
    return { predictedWeight: 'データがありません', chartData: [] };
  }

  const latestWeight = weightData[weightData.length - 1].weight;
  const predictedWeight = latestWeight + (Math.random() * 2 * WEIGHT_FLUCTUATION - WEIGHT_FLUCTUATION); // ランダムな変動を加味

  // 過去30日間のデータと予測データをチャート用に整形
  const chartData = [['Date', 'Weight']];
  const startDate = new Date();
  startDate.setDate(startDate.getDate() - 30);

  const filteredData = weightData.filter(item => new Date(item.date) >= startDate);
  filteredData.forEach(item => {
    chartData.push([item.date, item.weight]);
  });

  // 30日後までの予測データを追加 (簡易的な直線予測)
  const lastDate = new Date(weightData[weightData.length - 1].date);
  for (let i = 1; i <= 30; i++) {
    const nextDate = new Date(lastDate);
    nextDate.setDate(lastDate.getDate() + i);
    const predictedNextWeight = latestWeight + (Math.random() * 2 * WEIGHT_FLUCTUATION - WEIGHT_FLUCTUATION) * (i / 10); // 徐々に変動幅を大きく
    chartData.push([Utilities.formatDate(nextDate, Session.getTimeZone(), 'yyyy-MM-dd'), predictedNextWeight]);
  }

  return { predictedWeight: predictedWeight.toFixed(1), chartData: chartData };
}

1.と同様、シート名を変える必要があれば編集してください。

4. デプロイ

最後にプロジェクトを web 上に公開します。

Deploy から New Deployment を選びます。

image.png

設定アイコンから Web app を選びます。

image.png

そのままデプロイに進むと認証許可を求められます。

image.png

ここではアクセスできるのは自分だけにしておきましょう。

image.png

Authorize access から認証許可を進めていくと URL が発行されます。
下の Web App にアプリの URL が用意されています。

image.png

URL を開くとアプリができています。思った通りの動作になるか確かめてみてください。

image.png

Tips:1 エラーが出た時

思った通りにならなくても心配しないでください。

例えば、画面上にエラーメッセージが表示されていたら、そのまま、 Gemini に内容を伝えて修正してもらいましょう。Canvas のプレビューも有効に使ってください。

image.png

image.png

image.png

エラーメッセージでなくても、現象を伝えれば解決してくれることがあります。

image.png

コードを直したら、また New Deployment でデプロイしなおせばOKです!

Tips:2 Webアプリをつくれる生成 AI のサービス

他にもいろいろなサービスがあります。

https://v0.dev/

https://replit.com/

https://devin.ai/

Discussion