MENU

【AI初心者必見】Claudeに丸投げでOK!5分で完成する「社内リソース予約Bot」構築マニュアル

「プログラミングなんて1ミリもわからない。でも、便利なツールは作りたい。」

そんなワガママ、最新AIの Claude なら叶えられます。
本記事では、AIに「こんなの作りたい」と伝えるだけで、プログラムの作成からセットアップ手順の解説まで、すべてを丸投げしてアプリを完成させる方法を公開します。
あなたがやることは、Claudeが出した指示通りに「コピペ」するだけ。
専門知識ゼロから、現場で即戦力になる予約システムを手に入れる驚きの体験を、今すぐ始めましょう!

目次

1. 概要:AIに「丸投げ」してツールを作る新常識

これまでのアプリ開発は、コードを学び、エラーと戦う孤独な作業でした。しかし、これからは違います。
「Claudeに相談しながら、一緒に作る」。これが最短・最速のツール作成術です。

今回作成する「社内リソース予約Bot」も、Claudeとの数回のやり取りだけで完成しました。

  • Claudeがコードを書く: あなたの代わりに、複雑なプログラムをすべて作成します。
  • Claudeが手順を教える: どこに何を貼り付ければいいか、手取り足取りガイドしてくれます。
  • あなたはコピペするだけ: 難しい判断は一切不要です。

Claudeとの対話
▲ 「会議室予約Botを作りたい」と伝えるだけで、開発がスタートします

2. 実践:Claudeに「作り方」を聞いてみよう

まずは、Claudeにあなたの要望を伝えます。動画では、以下のようなプロンプト(指示)から始まりました。

プロンプト例:
社内で会議室や社用車の予定の管理をGoogleカレンダーで管理しています。

会議室1や会議室2とかをそれぞれ1つのGoogleカレンダーでそれぞれ管理してるんですけど、それをGASで作成したチャットBotから、現在空いている予定を聞いて空いていればそのままその予定を抑えるっていうのをしたいです。

GeminiのAPIを使ってチャットBotを作り、社内のGoogleカレンダーを抑えられるようなGASを書いてください。

スクリプトファイルだけじゃなく、htmlファイルも作って、Webアプリとしてデプロイできるようにしてください。

これだけで、Claudeは必要なプログラム(Code.gsとIndex.html)と、丁寧な 「セットアップガイド」 を一瞬で生成してくれます。

Claudeの回答
▲ 必要なファイル構成から手順まで、すべてClaudeが提案してくれます

3. セットアップ:Claudeの指示通りにコピペするだけ

Claudeが生成してくれた「セットアップガイド」に従って、作業を進めましょう。

ステップ1:GASプロジェクトの作成

  1. Google Apps Script を開き、「新しいプロジェクト」 を作成します。
  2. Claudeが書いた Code.gs の内容をコピーして、エディタに貼り付けます。

ステップ2:HTMLファイルの作成

  1. 左メニューの「+」から「HTML」を選択し、名前を Index にします。
  2. Claudeが書いた Index.html の内容をコピーして貼り付けます。

セットアップガイド
▲ Claudeが作成したガイドを見ながら進めれば、迷うことはありません


4. 仕上げ:自分専用にカスタマイズ

最後に、Claudeに教えてもらった通りに、あなたの環境に合わせた設定を行います。

  1. Gemini APIキーの入力: Google AI Studio で取得したキーを貼り付けます。
  2. カレンダーIDの設定: 予約したい会議室や社用車のカレンダーIDを、Claudeが指定した場所に貼り付けます。

これだけで、あなた専用のAI予約Botが動き出します!

5. 動作確認:AIが作ったツールの実力

完成したURLにアクセスして、実際に話しかけてみてください。

  • 「今日の会議室の空きを教えて」
  • 「会議室1を15時から予約して」


▲ あなたが書いたコードは1行もありませんが、完璧に動作します

6. まとめ:次はあなたに何を作らせますか?

今回のメインは、GASのコードそのものではありません。「AI(Claude)を使えば、やりたいことがこんなに簡単に形になる」 という体験です。

「メールを自動で仕分けたい」
「請求書を自動で作りたい」……。

そんなアイデアが浮かんだら、すぐにClaudeに相談してみてください。
「Claudeに聞きながら、コピペで作る」。この最強の武器を手に入れたあなたに、もう作れないツールはありません。

🎁 プレゼント:今回Claudeが作成したコード一式

マニュアルの最後に、今回の動画でClaudeが生成したコードを参考資料として掲載します。まずはこれをコピペして、AI開発の凄さを体感してみてください!
Code.gs(サーバーサイド)

// ============================================================
// 📌 設定セクション - ここを環境に合わせて編集してください
// ============================================================

/**
 * Gemini APIキー
 * Google AI Studio (https://aistudio.google.com/apikey) で取得
 */
const GEMINI_API_KEY = 'YOUR_GEMINI_API_KEY';

/**
 * Gemini モデル名
 */
const GEMINI_MODEL = 'gemini-2.0-flash';

/**
 * リソースカレンダーの定義
 * id: GoogleカレンダーのカレンダーID(カレンダーの設定画面から取得)
 * name: チャットで表示する名前
 * type: "room"(会議室)または "car"(社用車)など自由に設定可
 *
 * 【設定方法】
 * 1. Google カレンダーを開く
 * 2. 左サイドバーで対象カレンダーの「︙」→「設定と共有」
 * 3.「カレンダーの統合」セクションの「カレンダーID」をコピー
 * 4. 下記の id に貼り付ける
 */
const RESOURCES = [
  { id: 'CALENDAR_ID_FOR_ROOM_1@group.calendar.google.com', name: '会議室1', type: 'room' },
  { id: 'CALENDAR_ID_FOR_ROOM_2@group.calendar.google.com', name: '会議室2', type: 'room' },
  { id: 'CALENDAR_ID_FOR_CAR_1@group.calendar.google.com',  name: '社用車A', type: 'car'  },
  // 必要に応じて追加:
  // { id: 'xxx@group.calendar.google.com', name: '会議室3', type: 'room' },
];

// ============================================================
// 📌 Webアプリ エントリポイント
// ============================================================

function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index')
    .setTitle('社内リソース予約Bot')
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

// ============================================================
// 📌 クライアントから呼ばれるメイン関数
// ============================================================

/**
 * チャットメッセージを処理してレスポンスを返す
 * @param {string} userMessage - ユーザーの入力テキスト
 * @param {Array} conversationHistory - これまでの会話履歴
 * @return {Object} { reply: string, action: object|null, history: Array }
 */
function processMessage(userMessage, conversationHistory) {
  try {
    // 現在時刻の情報を付与
    const now = new Date();
    const timeInfo = Utilities.formatDate(now, Session.getScriptTimeZone(), 'yyyy年MM月dd日 (E) HH:mm');

    // リソース一覧テキスト
    const resourceList = RESOURCES.map(r => `- ${r.name}(タイプ: ${r.type})`).join('\n');

    // Geminiへのシステムプロンプト
    const systemPrompt = `あなたは社内リソース(会議室・社用車)の予約を手伝うアシスタントBotです。
現在日時: ${timeInfo}

利用可能なリソース:
${resourceList}

あなたの役割:
1. ユーザーが空き状況を確認したい場合 → check_availability アクションを返す
2. ユーザーが予約したい場合 → create_booking アクションを返す
3. その他の質問 → 通常の会話で対応

必ず以下のJSON形式で回答してください(JSON以外は絶対に含めないでください):
{
  "reply": "ユーザーへの返答テキスト",
  "action": null または アクションオブジェクト
}

■ アクションオブジェクトの形式:

【空き状況確認】
{
  "type": "check_availability",
  "resource_names": ["会議室1", "会議室2"],  // 確認したいリソース名の配列。「全部」「すべて」の場合は全リソース名を含める
  "date": "2025-01-15",                      // YYYY-MM-DD形式
  "start_time": "10:00",                     // HH:mm形式(省略可。省略時は終日の空きを確認)
  "end_time": "11:00"                        // HH:mm形式(省略可)
}

【予約作成】
{
  "type": "create_booking",
  "resource_name": "会議室1",     // 予約するリソース名(1つ)
  "date": "2025-01-15",
  "start_time": "10:00",
  "end_time": "11:00",
  "title": "予約のタイトル"       // ユーザーが指定しない場合は適当に設定
}

■ 重要なルール:
- 日付や時間が不明確な場合は、ユーザーに確認してください(actionはnull)
- 「明日」「来週月曜」など相対的な日時は、現在日時から計算してください
- 「今日の午後」のような曖昧な指定は確認を求めてください
- 予約する前に空き状況を確認することを推奨してください
- 回答は親しみやすく、簡潔にしてください
- レスポンスは必ず有効なJSON形式のみで返してください`;

    // 会話履歴を構築
    const messages = [];
    if (conversationHistory && conversationHistory.length > 0) {
      conversationHistory.forEach(msg => {
        messages.push({ role: msg.role, parts: [{ text: msg.text }] });
      });
    }
    messages.push({ role: 'user', parts: [{ text: userMessage }] });

    // Gemini API 呼び出し
    const geminiResponse = callGemini_(systemPrompt, messages);

    // JSONパース
    let parsed;
    try {
      // コードブロックで囲まれている場合に対応
      let cleaned = geminiResponse.trim();
      cleaned = cleaned.replace(/^```json\s*/i, '').replace(/^```\s*/i, '').replace(/\s*```$/i, '');
      parsed = JSON.parse(cleaned);
    } catch (e) {
      // JSONパースに失敗した場合はテキストとして返す
      parsed = { reply: geminiResponse, action: null };
    }

    let reply = parsed.reply || '';
    let action = parsed.action || null;

    // アクションの実行
    if (action) {
      if (action.type === 'check_availability') {
        const result = checkAvailability_(action);
        reply = result;
        action = { type: 'availability_result', data: result };
      } else if (action.type === 'create_booking') {
        const result = createBooking_(action);
        reply = result;
        action = { type: 'booking_result', data: result };
      }
    }

    // 会話履歴を更新
    const updatedHistory = conversationHistory ? [...conversationHistory] : [];
    updatedHistory.push({ role: 'user', text: userMessage });
    updatedHistory.push({ role: 'model', text: reply });

    // 履歴が長くなりすぎないように制限(直近20件)
    const trimmedHistory = updatedHistory.slice(-20);

    return {
      reply: reply,
      action: action,
      history: trimmedHistory
    };

  } catch (error) {
    Logger.log('Error in processMessage: ' + error.toString());
    return {
      reply: 'エラーが発生しました: ' + error.message + '\n設定を確認してください。',
      action: null,
      history: conversationHistory || []
    };
  }
}

// ============================================================
// 📌 Gemini API 呼び出し
// ============================================================

function callGemini_(systemPrompt, messages) {
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`;

  const payload = {
    system_instruction: {
      parts: [{ text: systemPrompt }]
    },
    contents: messages,
    generationConfig: {
      temperature: 0.3,
      maxOutputTokens: 2048
    }
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  const json = JSON.parse(response.getContentText());

  if (json.error) {
    throw new Error('Gemini API Error: ' + json.error.message);
  }

  return json.candidates[0].content.parts[0].text;
}

// ============================================================
// 📌 空き状況確認
// ============================================================

function checkAvailability_(action) {
  const resourceNames = action.resource_names || [];
  const dateStr = action.date;
  const startTimeStr = action.start_time;
  const endTimeStr = action.end_time;

  // 対象リソースを取得
  let targetResources = RESOURCES.filter(r => resourceNames.includes(r.name));
  if (targetResources.length === 0) {
    targetResources = RESOURCES; // 指定がない場合は全部
  }

  const tz = Session.getScriptTimeZone();
  let startDt, endDt;

  if (startTimeStr && endTimeStr) {
    // 特定の時間帯
    startDt = new Date(`${dateStr}T${startTimeStr}:00`);
    endDt = new Date(`${dateStr}T${endTimeStr}:00`);
  } else {
    // 終日(営業時間: 9:00-18:00)
    startDt = new Date(`${dateStr}T09:00:00`);
    endDt = new Date(`${dateStr}T18:00:00`);
  }

  const results = [];

  targetResources.forEach(resource => {
    try {
      const calendar = CalendarApp.getCalendarById(resource.id);
      if (!calendar) {
        results.push(`❌ ${resource.name}: カレンダーが見つかりません(IDを確認してください)`);
        return;
      }

      const events = calendar.getEvents(startDt, endDt);

      if (events.length === 0) {
        const timeRange = startTimeStr && endTimeStr
          ? `${startTimeStr}〜${endTimeStr}`
          : '9:00〜18:00';
        results.push(`✅ ${resource.name}: ${timeRange} は空いています!`);
      } else {
        const busyTimes = events.map(e => {
          const s = Utilities.formatDate(e.getStartTime(), tz, 'HH:mm');
          const eEnd = Utilities.formatDate(e.getEndTime(), tz, 'HH:mm');
          const title = e.getTitle() || '(タイトルなし)';
          return `  ・${s}〜${eEnd}「${title}」`;
        });
        results.push(`📅 ${resource.name}: 以下の予定があります\n${busyTimes.join('\n')}`);

        // 空き時間帯も計算して表示
        const freeSlots = findFreeSlots_(events, startDt, endDt, tz);
        if (freeSlots.length > 0) {
          const freeText = freeSlots.map(s => `  ・${s.start}〜${s.end}`).join('\n');
          results.push(`  → 空き時間帯:\n${freeText}`);
        } else {
          results.push(`  → この時間帯に空きはありません`);
        }
      }
    } catch (e) {
      results.push(`❌ ${resource.name}: エラー - ${e.message}`);
    }
  });

  const dateFormatted = Utilities.formatDate(new Date(dateStr + 'T00:00:00'), tz, 'yyyy年MM月dd日 (E)');
  const header = `📋 **${dateFormatted}** の空き状況:\n\n`;

  return header + results.join('\n\n') + '\n\n予約したい場合は「○○を△△時〜△△時で予約して」とお伝えください!';
}

/**
 * 予定の隙間から空き時間を計算する
 */
function findFreeSlots_(events, rangeStart, rangeEnd, tz) {
  // イベントを開始時刻でソート
  const sorted = events.slice().sort((a, b) => a.getStartTime() - b.getStartTime());

  const slots = [];
  let current = rangeStart;

  sorted.forEach(event => {
    const eStart = event.getStartTime();
    const eEnd = event.getEndTime();

    if (eStart > current) {
      // 30分以上の空きがある場合のみ表示
      if ((eStart - current) >= 30 * 60 * 1000) {
        slots.push({
          start: Utilities.formatDate(current, tz, 'HH:mm'),
          end: Utilities.formatDate(eStart, tz, 'HH:mm')
        });
      }
    }
    if (eEnd > current) {
      current = eEnd;
    }
  });

  // 最後のイベント後の空き
  if (rangeEnd > current && (rangeEnd - current) >= 30 * 60 * 1000) {
    slots.push({
      start: Utilities.formatDate(current, tz, 'HH:mm'),
      end: Utilities.formatDate(rangeEnd, tz, 'HH:mm')
    });
  }

  return slots;
}

// ============================================================
// 📌 予約作成
// ============================================================

function createBooking_(action) {
  const resourceName = action.resource_name;
  const dateStr = action.date;
  const startTimeStr = action.start_time;
  const endTimeStr = action.end_time;
  const title = action.title || '予約';

  // リソースを検索
  const resource = RESOURCES.find(r => r.name === resourceName);
  if (!resource) {
    return `❌ 「${resourceName}」というリソースが見つかりません。\n利用可能なリソース: ${RESOURCES.map(r => r.name).join('、')}`;
  }

  try {
    const calendar = CalendarApp.getCalendarById(resource.id);
    if (!calendar) {
      return `❌ ${resource.name}のカレンダーにアクセスできません。カレンダーIDを確認してください。`;
    }

    const startDt = new Date(`${dateStr}T${startTimeStr}:00`);
    const endDt = new Date(`${dateStr}T${endTimeStr}:00`);

    // 予約前に空きを再確認
    const existingEvents = calendar.getEvents(startDt, endDt);
    if (existingEvents.length > 0) {
      const tz = Session.getScriptTimeZone();
      const conflicts = existingEvents.map(e => {
        const s = Utilities.formatDate(e.getStartTime(), tz, 'HH:mm');
        const eEnd = Utilities.formatDate(e.getEndTime(), tz, 'HH:mm');
        return `${s}〜${eEnd}「${e.getTitle() || '(タイトルなし)'}」`;
      });
      return `⚠️ ${resource.name}は指定の時間帯に既に予約が入っています:\n${conflicts.join('\n')}\n\n別の時間帯をご検討ください。`;
    }

    // 予約を作成
    const event = calendar.createEvent(title, startDt, endDt);
    const tz = Session.getScriptTimeZone();
    const dateFormatted = Utilities.formatDate(startDt, tz, 'yyyy年MM月dd日 (E)');

    return `🎉 予約が完了しました!\n\n` +
           `📌 リソース: ${resource.name}\n` +
           `📅 日付: ${dateFormatted}\n` +
           `🕐 時間: ${startTimeStr}〜${endTimeStr}\n` +
           `📝 タイトル: ${title}\n\n` +
           `Googleカレンダーに登録されました。`;

  } catch (e) {
    return `❌ 予約の作成に失敗しました: ${e.message}`;
  }
}

// ============================================================
// 📌 ユーティリティ(テスト用)
// ============================================================

/**
 * リソース一覧を取得(デバッグ用)
 */
function getResourceList() {
  return RESOURCES.map(r => ({ name: r.name, type: r.type }));
}

/**
 * 設定テスト - カレンダーへのアクセスを確認
 */
function testCalendarAccess() {
  const results = [];
  RESOURCES.forEach(r => {
    try {
      const cal = CalendarApp.getCalendarById(r.id);
      if (cal) {
        results.push(`✅ ${r.name}: アクセスOK (${cal.getName()})`);
      } else {
        results.push(`❌ ${r.name}: カレンダーが見つかりません`);
      }
    } catch (e) {
      results.push(`❌ ${r.name}: エラー - ${e.message}`);
    }
  });
  Logger.log(results.join('\n'));
  return results;
}

Index.html(チャット画面)

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <style>
    /* ============================================================
       リセット & ベース
       ============================================================ */
    * { margin: 0; padding: 0; box-sizing: border-box; }

    :root {
      --primary: #2563eb;
      --primary-light: #dbeafe;
      --bg: #f3f4f6;
      --surface: #ffffff;
      --text: #1f2937;
      --text-muted: #6b7280;
      --border: #e5e7eb;
      --bot-bg: #f0f4ff;
      --user-bg: #2563eb;
      --user-text: #ffffff;
      --shadow: 0 1px 3px rgba(0,0,0,0.08);
      --radius: 12px;
    }

    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans JP', sans-serif;
      background: var(--bg);
      color: var(--text);
      height: 100vh;
      display: flex;
      flex-direction: column;
    }

    /* ============================================================
       ヘッダー
       ============================================================ */
    .header {
      background: var(--surface);
      border-bottom: 1px solid var(--border);
      padding: 16px 20px;
      display: flex;
      align-items: center;
      gap: 12px;
      box-shadow: var(--shadow);
      flex-shrink: 0;
    }

    .header-icon {
      width: 40px;
      height: 40px;
      background: var(--primary);
      border-radius: 10px;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 20px;
      color: white;
    }

    .header-text h1 {
      font-size: 16px;
      font-weight: 700;
    }

    .header-text p {
      font-size: 12px;
      color: var(--text-muted);
    }

    /* ============================================================
       チャットエリア
       ============================================================ */
    .chat-area {
      flex: 1;
      overflow-y: auto;
      padding: 20px;
      display: flex;
      flex-direction: column;
      gap: 16px;
      scroll-behavior: smooth;
    }

    .message {
      display: flex;
      gap: 10px;
      max-width: 85%;
      animation: fadeIn 0.3s ease;
    }

    @keyframes fadeIn {
      from { opacity: 0; transform: translateY(8px); }
      to   { opacity: 1; transform: translateY(0); }
    }

    .message.bot { align-self: flex-start; }
    .message.user { align-self: flex-end; flex-direction: row-reverse; }

    .message-avatar {
      width: 32px;
      height: 32px;
      border-radius: 50%;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 14px;
      flex-shrink: 0;
      margin-top: 2px;
    }

    .bot .message-avatar {
      background: var(--primary);
      color: white;
    }

    .user .message-avatar {
      background: #e5e7eb;
      color: var(--text);
    }

    .message-bubble {
      padding: 12px 16px;
      border-radius: var(--radius);
      line-height: 1.6;
      font-size: 14px;
      white-space: pre-wrap;
      word-break: break-word;
      box-shadow: var(--shadow);
    }

    .bot .message-bubble {
      background: var(--surface);
      border: 1px solid var(--border);
      border-top-left-radius: 4px;
    }

    .user .message-bubble {
      background: var(--user-bg);
      color: var(--user-text);
      border-top-right-radius: 4px;
    }

    /* ============================================================
       タイピングインジケーター
       ============================================================ */
    .typing-indicator {
      display: none;
      align-self: flex-start;
      gap: 10px;
      align-items: center;
    }

    .typing-indicator.show { display: flex; }

    .typing-dots {
      display: flex;
      gap: 4px;
      padding: 12px 16px;
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: var(--radius);
      border-top-left-radius: 4px;
      box-shadow: var(--shadow);
    }

    .typing-dots span {
      width: 8px;
      height: 8px;
      background: var(--text-muted);
      border-radius: 50%;
      animation: bounce 1.2s infinite;
    }

    .typing-dots span:nth-child(2) { animation-delay: 0.2s; }
    .typing-dots span:nth-child(3) { animation-delay: 0.4s; }

    @keyframes bounce {
      0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
      30% { transform: translateY(-6px); opacity: 1; }
    }

    /* ============================================================
       クイックアクション
       ============================================================ */
    .quick-actions {
      padding: 8px 20px;
      display: flex;
      gap: 8px;
      overflow-x: auto;
      flex-shrink: 0;
    }

    .quick-actions::-webkit-scrollbar { display: none; }

    .quick-btn {
      padding: 8px 14px;
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 20px;
      font-size: 13px;
      color: var(--primary);
      cursor: pointer;
      white-space: nowrap;
      transition: all 0.2s;
      flex-shrink: 0;
    }

    .quick-btn:hover {
      background: var(--primary-light);
      border-color: var(--primary);
    }

    /* ============================================================
       入力エリア
       ============================================================ */
    .input-area {
      background: var(--surface);
      border-top: 1px solid var(--border);
      padding: 16px 20px;
      display: flex;
      gap: 10px;
      align-items: flex-end;
      flex-shrink: 0;
    }

    .input-wrapper {
      flex: 1;
      position: relative;
    }

    #messageInput {
      width: 100%;
      padding: 12px 16px;
      border: 1px solid var(--border);
      border-radius: var(--radius);
      font-size: 14px;
      font-family: inherit;
      resize: none;
      outline: none;
      min-height: 44px;
      max-height: 120px;
      line-height: 1.5;
      transition: border-color 0.2s;
    }

    #messageInput:focus {
      border-color: var(--primary);
      box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
    }

    #messageInput::placeholder {
      color: var(--text-muted);
    }

    #sendBtn {
      width: 44px;
      height: 44px;
      background: var(--primary);
      color: white;
      border: none;
      border-radius: 50%;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      transition: all 0.2s;
      flex-shrink: 0;
    }

    #sendBtn:hover { background: #1d4ed8; transform: scale(1.05); }
    #sendBtn:disabled { background: #93c5fd; cursor: not-allowed; transform: none; }

    #sendBtn svg {
      width: 20px;
      height: 20px;
    }

    /* ============================================================
       レスポンシブ
       ============================================================ */
    @media (max-width: 600px) {
      .message { max-width: 92%; }
      .header { padding: 12px 16px; }
      .chat-area { padding: 12px; gap: 12px; }
      .input-area { padding: 12px 16px; }
      .quick-actions { padding: 6px 16px; }
    }
  </style>
</head>
<body>

  <!-- ヘッダー -->
  <div class="header">
    <div class="header-icon">🏢</div>
    <div class="header-text">
      <h1>社内リソース予約Bot</h1>
      <p>会議室・社用車の空き確認&予約</p>
    </div>
  </div>

  <!-- チャットエリア -->
  <div class="chat-area" id="chatArea">
    <!-- 初期メッセージ -->
    <div class="message bot">
      <div class="message-avatar">🤖</div>
      <div class="message-bubble">こんにちは!社内リソース予約Botです 🏢

会議室や社用車の空き状況確認・予約ができます。
お気軽にお声がけください!

例えば…
・「明日の会議室の空き状況を教えて」
・「会議室1を明日10時〜11時で予約して」
・「社用車は今週金曜空いてる?」</div>
    </div>

    <!-- タイピングインジケーター -->
    <div class="typing-indicator" id="typingIndicator">
      <div class="message-avatar" style="width:32px;height:32px;border-radius:50%;background:var(--primary);color:white;display:flex;align-items:center;justify-content:center;font-size:14px;">🤖</div>
      <div class="typing-dots">
        <span></span><span></span><span></span>
      </div>
    </div>
  </div>

  <!-- クイックアクション -->
  <div class="quick-actions">
    <button class="quick-btn" onclick="sendQuick('今日の会議室の空き状況を教えて')">📋 今日の会議室の空き</button>
    <button class="quick-btn" onclick="sendQuick('明日の全リソースの空きを見たい')">📅 明日の全リソース</button>
    <button class="quick-btn" onclick="sendQuick('社用車は明日空いてる?')">🚗 社用車の空き確認</button>
    <button class="quick-btn" onclick="sendQuick('利用できるリソースの一覧を教えて')">📝 リソース一覧</button>
  </div>

  <!-- 入力エリア -->
  <div class="input-area">
    <div class="input-wrapper">
      <textarea id="messageInput"
                placeholder="メッセージを入力…"
                rows="1"
                onkeydown="handleKeydown(event)"
                oninput="autoResize(this)"></textarea>
    </div>
    <button id="sendBtn" onclick="sendMessage()" title="送信">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <line x1="22" y1="2" x2="11" y2="13"></line>
        <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
      </svg>
    </button>
  </div>

  <script>
    // ============================================================
    // 状態管理
    // ============================================================
    let conversationHistory = [];
    let isProcessing = false;

    // ============================================================
    // DOM要素
    // ============================================================
    const chatArea = document.getElementById('chatArea');
    const messageInput = document.getElementById('messageInput');
    const sendBtn = document.getElementById('sendBtn');
    const typingIndicator = document.getElementById('typingIndicator');

    // ============================================================
    // メッセージ送信
    // ============================================================
    function sendMessage() {
      const text = messageInput.value.trim();
      if (!text || isProcessing) return;

      // ユーザーメッセージを表示
      appendMessage('user', text);
      messageInput.value = '';
      autoResize(messageInput);

      // 処理中状態に
      setProcessing(true);

      // GASのサーバー関数を呼び出し
      google.script.run
        .withSuccessHandler(handleResponse)
        .withFailureHandler(handleError)
        .processMessage(text, conversationHistory);
    }

    function sendQuick(text) {
      if (isProcessing) return;
      messageInput.value = text;
      sendMessage();
    }

    // ============================================================
    // レスポンス処理
    // ============================================================
    function handleResponse(result) {
      setProcessing(false);

      if (result && result.reply) {
        appendMessage('bot', result.reply);
        conversationHistory = result.history || [];
      } else {
        appendMessage('bot', '応答を取得できませんでした。もう一度お試しください。');
      }
    }

    function handleError(error) {
      setProcessing(false);
      appendMessage('bot', '⚠️ エラーが発生しました: ' + (error.message || '不明なエラー') + '\n\nしばらくしてからもう一度お試しください。');
    }

    // ============================================================
    // UI操作
    // ============================================================
    function appendMessage(role, text) {
      const div = document.createElement('div');
      div.className = `message ${role}`;

      const avatar = document.createElement('div');
      avatar.className = 'message-avatar';
      avatar.textContent = role === 'bot' ? '🤖' : '👤';

      const bubble = document.createElement('div');
      bubble.className = 'message-bubble';
      bubble.textContent = text;

      div.appendChild(avatar);
      div.appendChild(bubble);

      // タイピングインジケーターの前に挿入
      chatArea.insertBefore(div, typingIndicator);
      scrollToBottom();
    }

    function setProcessing(processing) {
      isProcessing = processing;
      sendBtn.disabled = processing;
      messageInput.disabled = processing;
      typingIndicator.classList.toggle('show', processing);

      if (!processing) {
        messageInput.focus();
      }
      scrollToBottom();
    }

    function scrollToBottom() {
      requestAnimationFrame(() => {
        chatArea.scrollTop = chatArea.scrollHeight;
      });
    }

    // ============================================================
    // 入力ハンドラ
    // ============================================================
    function handleKeydown(e) {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault();
        sendMessage();
      }
    }

    function autoResize(el) {
      el.style.height = 'auto';
      el.style.height = Math.min(el.scrollHeight, 120) + 'px';
    }

    // 初期フォーカス
    window.addEventListener('load', () => messageInput.focus());
  </script>

</body>
</html>

セットアップガイド

# 社内リソース予約Bot - セットアップガイド

## 概要

Google Apps Script + Gemini API で動作するチャットBot型の社内リソース(会議室・社用車)予約システムです。
Webアプリとしてデプロイし、ブラウザからチャット形式で空き確認・予約ができます。

---

## ファイル構成

| ファイル | 説明 |
|---------|------|
| `Code.gs` | サーバーサイドロジック(Gemini API連携、カレンダー操作) |
| `Index.html` | チャットUI(Webアプリ画面) |

---

## セットアップ手順

### 1. GASプロジェクトの作成

1. [Google Apps Script](https://script.google.com/) にアクセス
2. 「新しいプロジェクト」をクリック
3. プロジェクト名を「社内リソース予約Bot」などに変更

### 2. ファイルの配置

1. デフォルトの `コード.gs` の中身を `Code.gs` の内容で置き換え
2. 左メニューの「+」→「HTML」をクリック → ファイル名を `Index` にする
3. `Index.html` の内容を貼り付け

### 3. Gemini APIキーの取得と設定

1. [Google AI Studio](https://aistudio.google.com/apikey) にアクセス
2. 「APIキーを作成」をクリック
3. 取得したAPIキーを `Code.gs` の以下の行に貼り付け:

```javascript
const GEMINI_API_KEY = 'ここにAPIキーを貼り付け';
```

### 4. リソースカレンダーの設定

各会議室・社用車のGoogleカレンダーIDを `Code.gs` の `RESOURCES` 配列に設定します。

**カレンダーIDの確認方法:**
1. Google カレンダーを開く
2. 左サイドバーで対象カレンダーの「︙」→「設定と共有」
3. 「カレンダーの統合」セクションの「カレンダーID」をコピー

```javascript
const RESOURCES = [
  { id: 'xxxx@group.calendar.google.com', name: '会議室1', type: 'room' },
  { id: 'yyyy@group.calendar.google.com', name: '会議室2', type: 'room' },
  { id: 'zzzz@group.calendar.google.com', name: '社用車A', type: 'car' },
];
```

> **💡 ポイント:** GASを実行するアカウントがカレンダーの編集権限を持っている必要があります。
> 共有カレンダーの場合は「変更および共有の管理権限」を付与してください。

### 5. 動作テスト

1. GASエディタで `testCalendarAccess` 関数を選択して▶実行
2. 「ログ」(Ctrl+Enter)を確認し、各カレンダーに✅がついていればOK
3. 初回実行時にGoogleアカウントの認証許可が求められます → 許可してください

### 6. Webアプリとしてデプロイ

1. 右上の「デプロイ」→「新しいデプロイ」
2. 歯車アイコン → 「ウェブアプリ」を選択
3. 以下を設定:
   - **説明:** 社内リソース予約Bot
   - **次のユーザーとして実行:** 自分
   - **アクセスできるユーザー:** 「自分のみ」または組織内の全員(Google Workspace環境の場合)
4. 「デプロイ」をクリック
5. 表示されたURLがチャットBotのアクセスURLです

---

## 使い方

チャット画面で自然言語で話しかけるだけです。

### 空き確認の例:
- 「今日の会議室の空きを教えて」
- 「明日の午後、会議室1は空いてる?」
- 「来週月曜の全リソースの空き状況」
- 「社用車は金曜日使える?」

### 予約の例:
- 「会議室1を明日10:00〜11:00で予約して」
- 「社用車Aを1月20日の9時から12時まで押さえたい」
- 「会議室2を今日の15時〜16時で"クライアントMTG"として予約」

---

## カスタマイズ

### リソースの追加・変更

`Code.gs` の `RESOURCES` 配列を編集するだけです。
追加後は再デプロイ(デプロイ→デプロイを管理→鉛筆アイコン→新バージョン)が必要です。

### Geminiモデルの変更

```javascript
const GEMINI_MODEL = 'gemini-2.0-flash';  // 高速・低コスト
// const GEMINI_MODEL = 'gemini-2.5-pro-preview-06-05';  // 高精度
```

### クイックアクションボタンの編集

`Index.html` 内の `.quick-actions` セクションのボタンを編集してください。

---

## トラブルシューティング

| 症状 | 対処法 |
|------|--------|
| 「カレンダーが見つかりません」 | カレンダーIDが正しいか確認。実行アカウントに共有されているか確認 |
| 「Gemini API Error」 | APIキーが正しいか確認。API有効化されているか確認 |
| 応答が遅い | Geminiの応答待ち。modelをflashにすると速くなります |
| 「予約の作成に失敗」 | カレンダーの編集権限があるか確認 |
| デプロイ後に変更が反映されない | 「デプロイを管理」→新バージョンとしてデプロイし直す |

---

## 注意事項

- Gemini APIの無料枠には利用制限があります(1分あたりのリクエスト数等)
- 予約の削除・変更はこのBotからはできません(Googleカレンダーから直接操作してください)
- GASの実行時間制限は6分です(通常の操作では超えません)


よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次