0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

爆速自動化】AIに要望を言うだけでGoogleフォームを自動生成するGASツールを作ってみた【商用フリー】

0
Posted at

最近、AIでGoogleスライドなどを作成するツールが流行っていますよね。
そこで私も「自分でも作ってみよう!」と思ったのですが、難易度が高く挫折してしまいました(あたりまえ)。

だけど、「Googleフォームなら自分でもできるんじゃね?」と思って作ったのが、今回紹介するツールです。

🛠️ 使用環境

  • GAS (Google Apps Script)

💡 仕組み

  1. AIにプロンプトを投げてJSONデータを作ってもらう
  2. 生成されたJSONをGASのウェブアプリに入力する
  3. GASが自動でGoogleフォームの作成を開始する

🚀 体験サイト

体験版はこちらからお試しいただけます。
👉 ごまだんご式v1 体験サイト

実際にAIで作成したjson
{ "title": "技術ブログ・アウトプット講座 受講後アンケート", "description": "「技術ブログ・アウトプット講座」をご受講いただきありがとうございました。講座の感想や、その後のアウトプットの活用状況についてお聞かせください。(お名前の入力は不要です)", "confirmationMessage": "アンケートへのご協力ありがとうございました。今後の講座運営の参考にさせていただきます。", "isQuiz": false, "lockMode": false, "themeColor": "grey", "items": [ { "type": "section", "title": "セクション1:受講時の感想(全員回答)", "pageNavigation": "CONTINUE" }, { "type": "radio", "title": "Q1:今回の参加方法を教えてください。", "required": true, "points": 0, "hasOtherOption": false, "choices": [ { "value": "現地参加" }, { "value": "オンライン(Zoom)参加" } ] }, { "type": "scale", "title": "Q2:講座全体の満足度はいかがでしたか?", "required": true, "points": 0, "scaleMin": 1, "scaleMax": 5, "leftLabel": "不満", "rightLabel": "満足" }, { "type": "radio", "title": "Q3:講座の内容の難易度はいかがでしたか?", "required": true, "points": 0, "hasOtherOption": false, "choices": [ { "value": "難しい" }, { "value": "ちょうどいい" }, { "value": "易しい" } ] }, { "type": "scale", "title": "Q4:講師の説明はわかりやすかったですか?", "required": true, "points": 0, "scaleMin": 1, "scaleMax": 5, "leftLabel": "わかりにくい", "rightLabel": "わかりやすい" }, { "type": "checkbox", "title": "Q5:特に参考になった内容を教えてください。(複数選択可)", "required": true, "points": 0, "hasOtherOption": true, "choices": [ { "value": "プラットフォームの基本的な概要や文化" }, { "value": "記事の具体的な書き方・Markdown記法" }, { "value": "効果的な記事の検索・閲覧のコツ" }, { "value": "いいねやフォローなどのコミュニティ機能" } ] }, { "type": "radio", "title": "Q6:講座の時間配分はいかがでしたか?", "required": true, "points": 0, "hasOtherOption": false, "choices": [ { "value": "時間が足りない(短い)" }, { "value": "ちょうどいい" }, { "value": "時間が余った(長い)" } ] }, { "type": "scale", "title": "Q7:配布されたテキストや資料の分かりやすさはいかがでしたか?", "required": true, "points": 0, "scaleMin": 1, "scaleMax": 5, "leftLabel": "わかりにくい", "rightLabel": "わかりやすい" }, { "type": "radio", "title": "Q8:講座の受講後、実際に記事執筆や閲覧などのアウトプット活動をしていますか?", "required": true, "points": 0, "hasOtherOption": false, "choices": [ { "value": "記事を書いたり、よく閲覧して活用している", "goToSectionTitle": "セクション2:活用できている人限定" }, { "value": "あまり活用できていない・これから使う予定である", "goToSectionTitle": "セクション3:まだ活用できていない人限定" } ] }, { "type": "section", "title": "セクション2:活用できている人限定", "pageNavigation": "セクション4:今後の要望・その他" }, { "type": "checkbox", "title": "Q9:具体的にどのように活用していますか?(複数選択可)", "required": true, "points": 0, "hasOtherOption": true, "choices": [ { "value": "自分の技術知識のアウトプット(記事投稿)" }, { "value": "開発時の備忘録・メモ代わりとしての利用" }, { "value": "トラブルシューティングや技術情報の検索・閲覧" }, { "value": "他のエンジニアへのフィードバックやコメント等の交流" } ] }, { "type": "paragraph", "title": "Q10:アウトプット活動をしてみて、どのような効果やメリットを感じましたか?", "required": false, "points": 0 }, { "type": "text", "title": "Q11:今後、ブログ等で発信してみたいテーマや技術分野があれば教えてください。", "required": false, "points": 0 }, { "type": "section", "title": "セクション3:まだ活用できていない人限定", "pageNavigation": "CONTINUE" }, { "type": "checkbox", "title": "Q12:現在、アウトプット活動をする上でハードルになっていることは何ですか?(複数選択可)", "required": true, "points": 0, "hasOtherOption": true, "choices": [ { "value": "記事に書くためのネタが見つからない" }, { "value": "Markdownなどの書き方にまだ不安がある" }, { "value": "記事をネット上に公開すること自体に心理的ハードルがある" }, { "value": "日々の業務や学習が忙しく、時間を確保できない" } ] }, { "type": "paragraph", "title": "Q13:今後、どのようなサポートや情報があればより活用できそうですか?", "required": false, "points": 0 }, { "type": "section", "title": "セクション4:今後の要望・その他" }, { "type": "checkbox", "title": "Q14:今後開催してほしい講座や、知りたいテーマはありますか?(複数選択可)", "required": true, "points": 0, "hasOtherOption": true, "choices": [ { "value": "初心者を脱出するためのMarkdown応用テクニック" }, { "value": "読まれる記事にするための構成・ライティング講座" }, { "value": "特定の技術(Python、JavaScript、クラウド等)に特化した勉強会" }, { "value": "執筆効率を上げるための便利な周辺ツールの解説" } ] }, { "type": "paragraph", "title": "Q15:最後に、講師や運営スタッフへのメッセージ、ご意見などがあればご自由にご記入ください。", "required": false, "points": 0 } ] }

【重要】
アクセスすると認証画面が表示され、Googleフォームの権限(作成・編集)を求められます。ツールを実行するために権限の許可をお願いします。

【安心安全】
利用者のメールアドレスなどの個人情報は一切収集していません。


📜 利用規約(自作コードを利用する方へ)

このツールは、完全無料で公開しています!
利用にあたっては、以下の5点を守ってください。

  • 自分用の改造はOK!

    • 自分の環境に合わせて、コードを自由にカスタマイズして大丈夫です。
  • バグ報告のお願い

    • もしバグを見つけたら、コメントなどで教えていただけると非常にありがたいです。
  • 二次配布・転売はNG

    • 転売や勝手な配布、自作発言は禁止です。改造したコードをネット上で配布するのもお控えください。
  • 業務での利用はOK!

    • 仕事や社内の業務効率化として使うのは自由です。
  • ツール自体の商用利用・販売はNG

    • このツールや改造コードを他人に販売したり、有料サービスに組み込んで利益を得る行為は禁止です。

⚠️ お約束(免責事項)
このツールを使って万が一データが消えたり、システムが止まったりしても責任は取れません。あくまで「自己責任」でのご利用をお願いいたします。


📖 使い方

操作イメージ

  1. 「プロンプトをコピー」 ボタンを押します。
  2. コピーしたプロンプトをAIに投げます。
    • ※プロンプトの最後に、どんなフォームを作りたいかの要望を記入してください。
  3. AIの指示に従ってJSONデータを生成させます。
  4. 作成されたJSONを入力欄に入れます(既存のテキストは消してください)。
  5. 必要に応じて、画面右側でJSONを編集することも可能です。
  6. 「Googleフォームを書き出す」 を押すと、自動でフォームが作成されます。

🔒 セキュリティが気になる方へ

「見知らぬウェブアプリに権限を渡すのは不安…」という方は、以下のソースコードを使って、ご自身のGoogleドライブ上でGASプロジェクトを作成してご利用ください。

コード.gs(クリックで展開)
function doGet() {
  // ==========================================
  // 【超軽量】短時間の過剰アクセス一律ブロック機能
  // ==========================================
  const cache = CacheService.getScriptCache();
  
  // 💡 設定:制限をかける秒数と、その間に許容する最大アクセス数
  const LIMIT_SECONDS = 10; 
  const MAX_REQUESTS  = 25; 
  
  const cacheKey = "global_rate_limit_counter";
  
  // 1. キャッシュから現在の合計アクセス数を取得
  let currentRequests = parseInt(cache.get(cacheKey) || "0", 10);
  
  if (currentRequests >= MAX_REQUESTS) {
    // 【ブロック発動】短時間にアクセスが集中したら、HTMLを読み込まずに即拒否
    return ContentService.createTextOutput("ちょっとアクセス多すぎ。少し待ってね。だいぶ上限厳しくしているから待ってね")
                         .setMimeType(ContentService.MimeType.TEXT);
  }
  
  // 2. カウントを1増やして、指定秒数だけキャッシュに保存
  currentRequests++;
  cache.put(cacheKey, currentRequests.toString(), LIMIT_SECONDS); 

  // ==========================================
  // 【正常処理】アクセスが正常な場合はHTMLを表示
  // ==========================================
  return HtmlService.createHtmlOutputFromFile('index')
                    .setTitle('ごまだんご式プロンプトv1')
                    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}


function createFormFromJson(jsonString) {
  var currentItemTitle = "フォームの基本設定";
  var currentItemIndex = 0;
  
  try {
    var data = JSON.parse(jsonString);
    var form = FormApp.create(data.title || '無題のフォーム');
    
    if (data.isQuiz === true) {
      form.setIsQuiz(true);
    }
    
    if (data.isQuiz === true && data.lockMode === true) {
      try {
        form.setRequiresSubmiterToSignIn(true);
      } catch(e) {
        Logger.log('ロックモード前提設定のスキップ: ' + e.toString());
      }
    }
    
    if (data.description) form.setDescription(data.description);
    if (data.confirmationMessage) form.setConfirmationMessage(data.confirmationMessage);
    
    var items = data.items || [];
    var sectionObjects = {};
    
    // ラジオボタンとプルダウンの管理配列
    var radioItemsToUpdate = [];
    var dropdownItemsToUpdate = [];

    // --- ファーストパス: すべてのアイテムを順番通りに作成 ---
    for (var i = 0; i < items.length; i++) {
      var item = items[i];
      currentItemIndex = i + 1;
      currentItemTitle = item.title || "無題の項目";
      
      if (item.type === 'section') {
        var pageBreak = form.addPageBreakItem().setTitle(item.title);
        sectionObjects[item.title] = pageBreak;
        pageBreak.setGoToPage(FormApp.PageNavigationType.CONTINUE);
        continue;
      }
      
      var formItem;
      switch (item.type) {
        case 'text':
          formItem = form.addTextItem().setTitle(item.title).setRequired(!!item.required);
          if (data.isQuiz === true && item.points !== undefined && parseInt(item.points, 10) > 0) {
            formItem.setPoints(parseInt(item.points, 10));
          }
          if (item.validation) {
            var valBuild = FormApp.createTextValidation();
            if (item.validation.mode === 'email') valBuild.requireTextIsEmail();
            if (item.validation.mode === 'number') valBuild.requireNumber();
            if (item.validation.mode === 'text_length' && item.validation.limit) valBuild.requireTextLengthLessThanOrEqualTo(item.validation.limit);
            formItem.setValidation(valBuild.build());
          }
          break;
          
        case 'paragraph':
          formItem = form.addParagraphTextItem().setTitle(item.title).setRequired(!!item.required);
          if (data.isQuiz === true && item.points !== undefined && parseInt(item.points, 10) > 0) {
            formItem.setPoints(parseInt(item.points, 10));
          }
          break;
          
        case 'date':
          formItem = form.addDateItem().setTitle(item.title).setRequired(!!item.required);
          break;
          
        case 'time':
          formItem = form.addTimeItem().setTitle(item.title).setRequired(!!item.required);
          break;
          
        case 'scale':
          formItem = form.addScaleItem().setTitle(item.title).setRequired(!!item.required);
          var min = item.scaleMin || 1;
          var max = item.scaleMax || 5;
          formItem.setBounds(min, max);
          if (item.leftLabel || item.rightLabel) {
            formItem.setLabels(item.leftLabel || '', item.rightLabel || '');
          }
          break;
          
        case 'radio':
          formItem = form.addMultipleChoiceItem().setTitle(item.title).setRequired(!!item.required);
          if (data.isQuiz === true && item.points !== undefined) {
            formItem.setPoints(parseInt(item.points, 10) || 0);
          }
          radioItemsToUpdate.push({ itemData: item, formItemObject: formItem, index: currentItemIndex });
          break;
          
        case 'dropdown':
          formItem = form.addListItem().setTitle(item.title).setRequired(!!item.required);
          if (data.isQuiz === true && item.points !== undefined) {
            formItem.setPoints(parseInt(item.points, 10) || 0);
          }
          dropdownItemsToUpdate.push({ itemData: item, formItemObject: formItem, index: currentItemIndex });
          break;
          
        case 'checkbox':
          formItem = form.addCheckboxItem().setTitle(item.title).setRequired(!!item.required);
          if (data.isQuiz === true && item.points !== undefined) {
            formItem.setPoints(parseInt(item.points, 10) || 0);
          }
          var rawChoices = item.choices || [];
          var formattedChoices = [];
          for (var c = 0; c < rawChoices.length; c++) {
            if (data.isQuiz === true) {
              formattedChoices.push(formItem.createChoice(rawChoices[c].value, !!rawChoices[c].isCorrect));
            } else {
              formattedChoices.push(formItem.createChoice(rawChoices[c].value));
            }
          }
          if (formattedChoices.length > 0) formItem.setChoices(formattedChoices);
          if (item.hasOtherOption) formItem.showOtherOption(true);
          // ※シャッフル機能(setShuffleChoices)を完全削除しました
          break;
      }
    }

    // --- セカンドパス(A): ラジオボタンの選択肢設定 ---
    for (var j = 0; j < radioItemsToUpdate.length; j++) {
      var targetItem = radioItemsToUpdate[j];
      var d = targetItem.itemData;
      var fItem = targetItem.formItemObject;
      var rawChoices = d.choices || [];
      var formattedChoices = [];
      
      currentItemIndex = targetItem.index;
      currentItemTitle = d.title || "無題のラジオボタン質問";
      
      if (data.isQuiz === true) {
        for (var c = 0; c < rawChoices.length; c++) {
          formattedChoices.push(fItem.createChoice(rawChoices[c].value, !!rawChoices[c].isCorrect));
        }
      } else {
        for (var c = 0; c < rawChoices.length; c++) {
          var choiceText = rawChoices[c].value;
          var branchTitle = rawChoices[c].goToSectionTitle ? rawChoices[c].goToSectionTitle.trim() : "";
          
          if (branchTitle !== "" && branchTitle !== "CONTINUE") {
            var target = branchTitle === 'SUBMIT' ? FormApp.PageNavigationType.SUBMIT : sectionObjects[branchTitle];
            if (target) {
              formattedChoices.push(fItem.createChoice(choiceText, target));
              continue;
            }
          }
          formattedChoices.push(fItem.createChoice(choiceText));
        }
      }
      if (formattedChoices.length > 0) fItem.setChoices(formattedChoices);
      if (d.type === 'radio' && d.hasOtherOption) fItem.showOtherOption(true);
      // ※シャッフル機能(setShuffleChoices)を完全削除しました
    }

    // --- セカンドパス(B): プルダウンの選択肢設定 ---
    for (var m = 0; m < dropdownItemsToUpdate.length; m++) {
      var targetItem = dropdownItemsToUpdate[m];
      var d = targetItem.itemData;
      var fItem = targetItem.formItemObject;
      var rawChoices = d.choices || [];
      var formattedChoices = [];
      
      currentItemIndex = targetItem.index;
      currentItemTitle = d.title || "無題のプルダウン質問";
      
      if (data.isQuiz === true) {
        for (var c = 0; c < rawChoices.length; c++) {
          formattedChoices.push(fItem.createChoice(rawChoices[c].value, !!rawChoices[c].isCorrect));
        }
      } else {
        for (var c = 0; c < rawChoices.length; c++) {
          var choiceText = rawChoices[c].value;
          var branchTitle = rawChoices[c].goToSectionTitle ? rawChoices[c].goToSectionTitle.trim() : "";
          
          if (branchTitle !== "" && branchTitle !== "CONTINUE") {
            var target = branchTitle === 'SUBMIT' ? FormApp.PageNavigationType.SUBMIT : sectionObjects[branchTitle];
            if (target) {
              formattedChoices.push(fItem.createChoice(choiceText, target));
              continue;
            }
          }
          formattedChoices.push(fItem.createChoice(choiceText));
        }
      }
      if (formattedChoices.length > 0) fItem.setChoices(formattedChoices);
    }

    // --- サードパス: セクション遷移確定 ---
    for (var k = 0; k < items.length; k++) {
      if (items[k].type === 'section' && items[k].pageNavigation) {
        var navStr = items[k].pageNavigation.trim();
        var sObj = sectionObjects[items[k].title];
        currentItemTitle = items[k].title;
        if (sObj) {
          if (navStr === 'SUBMIT') {
            sObj.setGoToPage(FormApp.PageNavigationType.SUBMIT);
          } else if (navStr === 'CONTINUE') {
            sObj.setGoToPage(FormApp.PageNavigationType.CONTINUE);
          } else if (sectionObjects[navStr]) {
            sObj.setGoToPage(sectionObjects[navStr]);
          }
        }
      }
    }
    
    return { success: true, id: form.getId(), editUrl: form.getEditUrl(), publishedUrl: form.getPublishedUrl() };
    
  } catch (error) {
    var detailedErrorMessage = "【GAS生成エラー】\n" +
                               "■ 発生場所: 項目 #" + currentItemIndex + "" + currentItemTitle + "\n" +
                               "■ エラー内容: " + error.toString();
    return { success: false, message: detailedErrorMessage };
  }
index.html(クリックで展開)
<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <style>
    /* 白と灰色を基調としたミニマルなフラットデザイン */
    body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; padding: 0; background: #ffffff; color: #1a1a1a; height: 100vh; display: flex; overflow: hidden; }
    
    /* 左右50%ずつの分離配置(z-indexや重なりのバグを完全防止) */
    .panel { width: 50%; height: 100vh; box-sizing: border-box; display: flex; flex-direction: column; padding: 24px; }
    .left-panel { border-right: 1px solid #e5e5e5; background: #f8f9fa; overflow-y: auto; }
    .right-panel { background: #ffffff; display: flex; flex-direction: column; overflow: hidden; }
    
    h2 { font-size: 16px; font-weight: 700; margin-top: 0; margin-bottom: 16px; color: #1a1a1a; display: flex; align-items: center; gap: 8px; }
    
    /* プロンプトコンテナ */
    .prompt-container { background: #ffffff; border: 1px solid #e5e5e5; border-radius: 4px; padding: 16px; margin-bottom: 20px; }
    .prompt-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; font-size: 13px; font-weight: 700; color: #404040; }
    
    /* 入力エリア */
    textarea { width: 100%; min-height: 180px; font-family: monospace; font-size: 12px; padding: 14px; border: 1px solid #d4d4d4; border-radius: 4px; box-sizing: border-box; background: #ffffff; color: #1a1a1a; resize: none; margin-bottom: 12px; }
    textarea:focus { border-color: #737373; outline: none; }

    /* 各種ボタン */
    .btn-group { display: flex; gap: 10px; margin-top: 4px; }
    .btn { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 36px; padding: 0 16px; border-radius: 4px; font-size: 13px; font-weight: 600; border: 1px solid #d4d4d4; cursor: pointer; background: #ffffff; color: #1a1a1a; user-select: none; }
    .btn-sm { height: 28px; padding: 0 10px; font-size: 11px; border-radius: 3px; }
    .btn-primary { background: #1a1a1a; color: #ffffff; border-color: #1a1a1a; }
    .btn-primary:hover { background: #404040; border-color: #404040; }
    .btn-success { background: #ffffff; color: #1a1a1a; border-color: #1a1a1a; }
    .btn-success:hover { background: #f5f5f5; }
    .btn-danger { background: #ffffff; color: #dc2626; border-color: #dc2626; width: 100%; margin-top: 12px; }
    .btn-danger:hover { background: #fef2f2; }
    
    /* 右側の編集エリア(ここだけを独立スクロール) */
    #visualEditor { flex-grow: 1; overflow-y: auto; padding-right: 4px; margin-bottom: 12px; }

    /* カードデザイン */
    .card { background: #ffffff; border: 1px solid #e5e5e5; border-radius: 4px; padding: 20px; margin-bottom: 16px; position: relative; box-shadow: 0 1px 2px rgba(0,0,0,0.02); }
    .card-header-accent { border-top: 3px solid #1a1a1a; }
    .card-section-accent { border-top: 3px solid #737373; background: #fafafa; }
    
    .field { margin-bottom: 14px; }
    .field-label { font-size: 11px; color: #737373; font-weight: 700; margin-bottom: 4px; display: block; }
    .input-text { width: 100%; border: none; border-bottom: 1px solid #d4d4d4; padding: 6px 0; font-size: 14px; background: transparent; box-sizing: border-box; color: #1a1a1a; }
    .input-text:focus { outline: none; border-bottom: 1px solid #1a1a1a; }
    .select-style { border: 1px solid #d4d4d4; padding: 4px 8px; font-size: 12px; border-radius: 4px; background: #ffffff; cursor: pointer; color: #1a1a1a; }
    
    /* 操作ボタン(右上) */
    .card-actions { position: absolute; top: 16px; right: 16px; display: flex; gap: 4px; }
    .action-icon { background: #ffffff; border: 1px solid #e5e5e5; padding: 2px 6px; cursor: pointer; color: #737373; display: inline-flex; align-items: center; border-radius: 3px; font-size: 11px; user-select: none; }
    .action-icon:hover { background: #f5f5f5; color: #1a1a1a; border-color: #737373; }
    .action-icon-delete:hover { background: #fef2f2; color: #dc2626; border-color: #dc2626; }
    
    /* 選択肢ブロック */
    .choice-block { background: #fafafa; border: 1px solid #e5e5e5; border-radius: 4px; padding: 12px; margin-top: 8px; }
    .choice-row { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; }
    .choice-row:last-child { margin-bottom: 0; }

    /* クイズ設定 */
    .quiz-badge { display: inline-flex; align-items: center; gap: 4px; font-size: 10px; font-weight: 700; background: #f5f5f5; color: #404040; padding: 2px 6px; border-radius: 3px; margin-bottom: 6px; border: 1px solid #e5e5e5; }
    .quiz-settings-row { display: flex; gap: 12px; align-items: center; background: #fcfcfc; padding: 10px; border-radius: 4px; margin-top: 10px; border: 1px solid #e5e5e5; }

    /* 下部ツールバー固定配置 */
    .editor-footer { background: #ffffff; padding-top: 12px; border-top: 1px solid #e5e5e5; display: flex; justify-content: center; gap: 10px; flex-shrink: 0; }
    
    .toggle-row { display: flex; align-items: center; justify-content: flex-end; gap: 12px; font-size: 13px; margin-top: 12px; border-top: 1px solid #e5e5e5; padding-top: 10px; }
    .success-banner { background: #fafafa; color: #1a1a1a; border: 1px solid #e5e5e5; padding: 20px; border-radius: 4px; margin-top: 16px; }
    .success-banner a { color: #1a1a1a; font-weight: 700; text-decoration: underline; display: inline-flex; align-items: center; gap: 4px; }
    
    .error-box { background: #fef2f2; border: 1px solid #fee2e2; border-radius: 4px; padding: 16px; color: #991b1b; }
    .error-msg { font-family: monospace; font-size: 12px; background: #ffffff; padding: 8px; border: 1px solid #fee2e2; white-space: pre-wrap; color: #dc2626; margin: 8px 0; }
    .prompt-text { font-family: monospace; background: #ffffff; padding: 12px; display: block; margin-top: 8px; user-select: all; cursor: pointer; white-space: pre-wrap; border-radius: 4px; border: 1px solid #e5e5e5; }
  </style>
</head>
<body>

<!-- 左ペイン -->
<div class="panel left-panel">
  <div class="prompt-container">
    <div class="prompt-header">
      <span>💡 AI用万能テスト・クイズ対応プロンプト</span>
      <button id="btnCopy" class="btn btn-sm btn-success" onclick="copyPromptToClipboard()">プロンプトをコピー</button>
    </div>
    <div style="font-size: 11px; color: #525252; line-height: 1.4;">
      ボタンを押してコピーし、AIに渡してください。むやみな必須回答化を防ぐロジックが組み込まれています。
    </div>
  </div>

  <h2>JSONデータ</h2>
  <textarea id="jsonInput" placeholder="AIから返ってきた ```json ... ``` をここに貼り付けてください..." oninput="syncJsonToVisual()"></textarea>
  
  <div class="btn-group">
    <button id="btnCreate" class="btn btn-primary" onclick="generateForm()">Googleフォームを書き出す</button>
  </div>
  
  <div id="status" style="margin-top:12px; font-size:13px; font-weight:bold; display:none;"></div>
  <div id="successArea"></div>
</div>

<!-- 右ペイン -->
<div class="panel right-panel">
  <h2>フォーム編集・構築画面 (プレビュー)</h2>
  <div id="visualEditor"></div>
  
  <div class="editor-footer">
    <button class="btn btn-sm" onclick="addNewItem('text')">+ 質問を追加</button>
    <button class="btn btn-sm" onclick="addNewItem('radio')">+ 選択肢質問を追加</button>
    <button class="btn btn-sm" style="border-color:#737373;" onclick="addNewItem('section')">+ セクションを追加</button>
  </div>
</div>

<script>
var currentFormId = null;
var currentData = { title: "新規フォーム", description: "", confirmationMessage: "", isQuiz: false, lockMode: false, items: [] };

var embeddedPromptText = `【厳守:一発目のJSON出力は絶対禁止】
あなたはGoogleフォームの設計専門家です。最初の回答でいきなりJSONを出力することは【絶対に禁止】します。必ず以下のステップに従ってください。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
■ ステップ1:構成案の提案(あなたの最初の回答)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
指示を読み取り、以下の3点を箇条書きで簡潔に提案してください。
1. 【全体方針】:1ページ完結か、複数ページ(セクション区切り)か。
2. 【条件分岐の要否】:回答によるページジャンプや合流ロジックが必要かどうかの判断。
3. 【構成案】:各ページに配置する質問項目の一覧。(それぞれの問題を必須にするか)
4. 【構成案の再確認】:各ページに配置する質問項目をみて無意味な "SUBMIT" が置かれていないか。原則(条件分岐を使わない場合) "SUBMIT" は使わない


最後に必ず「この構成で進めてよろしいですか?問題なければ『OK』と指示してください」と問いかけ、出力を止めてください。

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
■ ステップ2:調整とJSON出力(ユーザーが『OK』と言ったら実行)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ユーザーから承諾(OK等)を得るまでは、質問の調整を繰り返してください。
承諾を得た段階で、初めて最終構成を以下の【JSON仕様】に落とし込み、\`\`\`json ... \`\`\` のコードブロック【のみ】を出力してください。挨拶や解説は一切不要です。

────────────────────────────────────────
【JSON仕様(スキーマ定義)】
────────────────────────────────────────
{
  "title": "[タイトル]",
  "description": "[説明文]",
  "confirmationMessage": "[送信完了メッセージ]",
  "isQuiz": true | false, // テスト・クイズ・採点機能時は true
  "lockMode": true | false, // 「ロックモードにして」と明示された場合のみ true
  "themeColor": "grey",
  "items": [
    // 以下の【部品】を指定された規模に応じて必要な数だけ連結して配置せよ
    
    // 1. セクション区切り
    { "type": "section", "title": "[タイトル]", "pageNavigation": "CONTINUE" | "SUBMIT" | "[合流先セクション名]" },
    // 2. 記述式・短文
    { "type": "text", "title": "[質問名]", "required": true | false, "points": 10 },
    // 3. 記述式・長文 / 日付 / 時刻
    { "type": "paragraph" | "date" | "time", "title": "[質問名]", "required": true | false, "points": 10 },
    // 4. 段階評価
    { "type": "scale", "title": "[質問名]", "required": true | false, "scaleMin": 1, "scaleMax": 5, "leftLabel": "[最小説明]", "rightLabel": "[最大説明]" },
    // 5. 選択肢(ラジオ/チェック/プルダウン)
   ■ 5. 選択肢(ラジオ/チェック/プルダウン)
{
  "type": "radio" | "checkbox" | "dropdown",
  "title": "[質問名]",
  "required": true | false,
  "hasOtherOption": true | false,
  "choices": [
    // 問題の選択肢を必要な数だけ配列で並べる。1番目、2番目に関わらず、本当に正しい答えのオブジェクトにだけ「"isCorrect": true」を正確に配置せよ。
    { "value": "[選択肢A(これが正解ならここにisCorrectを入れる)]", "isCorrect": true },
    { "value": "[選択肢B(これが不正解ならisCorrectは書かない)]" },
    { "value": "[選択肢C]", "goToSectionTitle": "SUBMIT" | "[ジャンプ先セクション名]" } // goToSectionTitleは分岐時のみ
  ]
}

【⚠️ クイズ(isQuiz: true)時の正解フラグ設置ルール(絶対厳守)】
・テンプレートの並び順に惑わされず、あなたが作成したクイズの「本当の正解のテキスト」を持つ選択肢オブジェクトにだけ、正確に「"isCorrect": true」を仕込んでください。
・1番目の選択肢(配列の先頭)ばかりを思考停止してすべて正解にする悪癖(バグ)を絶対に禁止します。問題ごとに、1番目、2番目、3番目、4番目と、正解の配置位置を完全にバラバラにシャッフルしてデータを出力してください。

  ]
}

【⚠️ 必須設定(required)の厳格ルール】
• 必須(true)にするのは、氏名、出席番号など、未入力だと運用が破綻する重要項目のみ。
• 「任意」「感想」「自由記述」の項目、および「配点0(points: 0)」のアンケート項目は、100%例外なく「\"required\": false」にせよ。
「【超重要】クイズ問題(設問)が並ぶメインのページ(セクション)において、そのセクション自体の "pageNavigation" に "SUBMIT" を指定することは絶対に禁止します。クイズを配置するページの pageNavigation は必ず "CONTINUE" にするか、キー自体を省略してください。」

────────────────────────────────────────
【作成したいフォーム】
[ここに要望を記入してください]`;




window.onload = function() {
  document.getElementById('jsonInput').value = JSON.stringify(currentData, null, 2);
  renderVisualEditor(currentData);
};

function copyPromptToClipboard() {
  navigator.clipboard.writeText(embeddedPromptText).then(function() {
    var btn = document.getElementById('btnCopy');
    btn.innerText = "コピー完了";
    setTimeout(function() { btn.innerText = "プロンプトをコピー"; }, 2000);
  });
}

function syncJsonToVisual() {
  var jsonStr = document.getElementById('jsonInput').value.trim();
  if (!jsonStr) return;
  
  jsonStr = jsonStr.replace(/^```json\s*/i, '').replace(/```$/, '').trim();
  try {
    var parsed = JSON.parse(jsonStr);
    currentData = parsed;
    if (!currentData.items) currentData.items = [];
    if (document.activeElement.id === 'jsonInput') {
      renderVisualEditor(currentData);
    }
  } catch(e) {
    document.getElementById('visualEditor').innerHTML = '<div class="error-box"><div class="error-title">JSON構文エラー</div><div class="error-msg">' + e.message + '</div></div>';
  }
}

function syncVisualToJson() {
  document.getElementById('jsonInput').value = JSON.stringify(currentData, null, 2);
}

function renderVisualEditor(data) {
  var container = document.getElementById('visualEditor');
  container.innerHTML = '';
  
  var quizChecked = data.isQuiz ? 'checked' : '';
  var lockChecked = data.lockMode ? 'checked' : '';
  
  var headerCard = document.createElement('div');
  headerCard.className = 'card card-header-accent';
  headerCard.innerHTML = `
    <div class="field"><div class="field-label">フォームのタイトル</div><input type="text" class="input-text" data-key="title" value="${data.title || ''}"></div>
    <div class="field"><div class="field-label">フォームの説明文</div><input type="text" class="input-text" data-key="description" value="${data.description || ''}"></div>
    <div class="field"><div class="field-label">送信完了メッセージ</div><input type="text" class="input-text" data-key="confirmationMessage" value="${data.confirmationMessage || ''}"></div>
    <div style="display:flex; gap:20px; background:#f5f5f5; padding:10px; border-radius:4px; font-size:12px; border:1px solid #e5e5e5;">
      <label style="cursor:pointer;"><input type="checkbox" id="mainIsQuiz" ${quizChecked}> 🖩 テストモード</label>
      <label style="cursor:pointer;"><input type="checkbox" id="mainLockMode" ${lockChecked}> 🔒 Chromebook ロック</label>
    </div>
  `;
  container.appendChild(headerCard);
  
  headerCard.querySelectorAll('.input-text').forEach(function(input) {
    input.addEventListener('input', function() { currentData[this.getAttribute('data-key')] = this.value; syncVisualToJson(); });
  });
  headerCard.querySelector('#mainIsQuiz').addEventListener('change', function() { currentData.isQuiz = this.checked; syncVisualToJson(); renderVisualEditor(currentData); });
  headerCard.querySelector('#mainLockMode').addEventListener('change', function() { currentData.lockMode = this.checked; syncVisualToJson(); });

  var items = data.items || [];
  items.forEach(function(item, index) {
    var card = document.createElement('div');
    var isSection = (item.type === 'section');
    card.className = 'card ' + (isSection ? 'card-section-accent' : '');
    
    var html = `
      <div class="card-actions">
        <button class="action-icon btn-up" data-idx="${index}">上へ</button>
        <button class="action-icon btn-down" data-idx="${index}">下へ</button>
        <button class="action-icon action-icon-delete btn-del" data-idx="${index}">削除</button>
      </div>
    `;
    
    if (isSection) {
      html += `
        <div class="field"><div class="field-label">セクション区切り</div><input type="text" class="input-text item-title" data-idx="${index}" value="${item.title || ''}"></div>
        <div class="field" style="margin-bottom:0;"><div class="field-label">次ページへの進路設定</div><input type="text" class="input-text item-nav" data-idx="${index}" value="${item.pageNavigation || 'CONTINUE'}"></div>
      `;
    } else {
      var types = { 'text':'短文記述', 'paragraph':'長文記述', 'radio':'ラジオボタン', 'checkbox':'チェックボックス', 'dropdown':'プルダウン', 'date':'日付', 'time':'時刻', 'scale':'段階評価' };
      var selectOptions = Object.keys(types).map(t => `<option value="${t}" ${item.type === t ? 'selected' : ''}>${types[t]}</option>`).join('');
      
      html += `
        <div style="margin-bottom:10px; display:flex; align-items:center; gap:8px;">
          <select class="select-style" data-idx="${index}" id="sel_${index}">${selectOptions}</select>
          ${data.isQuiz ? `<span class="quiz-badge">採点対象</span>` : ''}
        </div>
        <div class="field"><input type="text" class="input-text item-title" data-idx="${index}" placeholder="質問文..." value="${item.title || ''}"></div>
      `;
      
      if (data.isQuiz) {
        html += `
          <div class="quiz-settings-row">
            <span style="font-size:12px; font-weight:bold;">設問の配点:</span>
            <input type="number" class="input-text item-points" data-idx="${index}" style="width:50px; padding:0; text-align:center;" value="${item.points || 0}">
            <span style="font-size:12px;">点</span>
          </div>
        `;
      }
      
      if (['radio', 'checkbox', 'dropdown'].includes(item.type)) {
        html += `<div class="choice-block"><div class="field-label">選択肢(${data.isQuiz ? '正解指定' : '一覧'})</div><div class="choice-container"></div>`;
        html += `<button class="btn btn-sm btn-add-choice" data-idx="${index}">+ 選択肢を追加</button></div>`;
      }
      
      var checked = item.required ? 'checked' : '';
      html += `
        <div class="toggle-row">
          <label style="cursor:pointer;"><input type="checkbox" class="item-req" data-idx="${index}" ${checked}> 回答を必須にする</label>
        </div>
      `;
    }
    
    card.innerHTML = html;
    container.appendChild(card);
    
    card.querySelector('.btn-up').addEventListener('click', function() { moveItem(parseInt(this.getAttribute('data-idx'), 10), -1); });
    card.querySelector('.btn-down').addEventListener('click', function() { moveItem(parseInt(this.getAttribute('data-idx'), 10), 1); });
    card.querySelector('.btn-del').addEventListener('click', function() { removeItem(parseInt(this.getAttribute('data-idx'), 10)); });
    
    var txtTitle = card.querySelector('.item-title');
    if(txtTitle) txtTitle.addEventListener('input', function() { currentData.items[parseInt(this.getAttribute('data-idx'), 10)].title = this.value; syncVisualToJson(); });
    
    var txtNav = card.querySelector('.item-nav');
    if(txtNav) txtNav.addEventListener('input', function() { currentData.items[parseInt(this.getAttribute('data-idx'), 10)].pageNavigation = this.value; syncVisualToJson(); });
    
    var selType = card.querySelector('.select-style');
    if(selType) selType.addEventListener('change', function() { changeItemType(parseInt(this.getAttribute('data-idx'), 10), this.value); });
    
    var chkReq = card.querySelector('.item-req');
    if(chkReq) chkReq.addEventListener('change', function() { currentData.items[parseInt(this.getAttribute('data-idx'), 10)].required = this.checked; syncVisualToJson(); });
    
    var numPoints = card.querySelector('.item-points');
    if(numPoints) numPoints.addEventListener('input', function() { currentData.items[parseInt(this.getAttribute('data-idx'), 10)].points = parseInt(this.value,10)||0; syncVisualToJson(); });
    
    var choiceContainer = card.querySelector('.choice-container');
    if (choiceContainer) {
      var choices = item.choices || [];
      choices.forEach(function(choice, cIndex) {
        var cRow = document.createElement('div');
        cRow.className = 'choice-row';
        
        var correctChecked = choice.isCorrect ? 'checked' : '';
        // 【修正】混入していたエスケープ用の余計な記号を綺麗にパージしました
        cRow.innerHTML = `
          ${data.isQuiz ? '<input type="checkbox" class="c-correct" data-idx="' + index + '" data-cidx="' + cIndex + '" ' + correctChecked + '>' : ''}
          <input type="text" class="input-text c-val" data-idx="${index}" data-cidx="${cIndex}" style="font-size:13px; flex:2;" placeholder="テキスト" value="${choice.value || ''}">
          ${item.type !== 'checkbox' ? '<input type="text" class="input-text c-branch" data-idx="' + index + '" data-cidx="' + cIndex + '" style="font-size:12px; font-family:monospace; flex:1;" placeholder="分岐先" value="' + (choice.goToSectionTitle || '') + '">' : ''}
          <button class="btn btn-sm c-del" data-idx="${index}" data-cidx="${cIndex}" style="color:#dc2626; border:none; padding:0 4px;">✕</button>
        `;
        choiceContainer.appendChild(cRow);
        
        cRow.querySelector('.c-val').addEventListener('input', function() { 
          var pIdx = parseInt(this.getAttribute('data-idx'), 10);
          var cIdx = parseInt(this.getAttribute('data-cidx'), 10);
          currentData.items[pIdx].choices[cIdx].value = this.value; 
          syncVisualToJson(); 
        });
        
        var cBranch = cRow.querySelector('.c-branch');
        if(cBranch) cBranch.addEventListener('input', function() { 
          var pIdx = parseInt(this.getAttribute('data-idx'), 10);
          var cIdx = parseInt(this.getAttribute('data-cidx'), 10);
          updateChoiceBranch(pIdx, cIdx, this.value); 
        });
        
        var cCorrect = cRow.querySelector('.c-correct');
        if(cCorrect) cCorrect.addEventListener('change', function() { 
          var pIdx = parseInt(this.getAttribute('data-idx'), 10);
          var cIdx = parseInt(this.getAttribute('data-cidx'), 10);
          updateChoiceCorrect(pIdx, cIdx, this.checked); 
        });
        
        cRow.querySelector('.c-del').addEventListener('click', function() { 
          var pIdx = parseInt(this.getAttribute('data-idx'), 10);
          var cIdx = parseInt(this.getAttribute('data-cidx'), 10);
          removeChoice(pIdx, cIdx); 
        });
      });
      
      card.querySelector('.btn-add-choice').addEventListener('click', function() { 
        addChoiceOption(parseInt(this.getAttribute('data-idx'), 10)); 
      });
    }
  });
}

function changeItemType(idx, newType) {
  currentData.items[idx].type = newType;
  if (['radio', 'checkbox', 'dropdown'].includes(newType) && !currentData.items[idx].choices) {
    currentData.items[idx].choices = [{ value: "選択肢1", isCorrect: false }];
  }
  syncVisualToJson(); renderVisualEditor(currentData);
}
function updateChoiceBranch(idx, cIdx, val) { 
  if(!val) { delete currentData.items[idx].choices[cIdx].goToSectionTitle; } 
  else { currentData.items[idx].choices[cIdx].goToSectionTitle = val; }
  syncVisualToJson(); 
}
function updateChoiceCorrect(idx, cIdx, val) {
  if (currentData.items[idx].type !== 'checkbox' && val) {
    for(var x=0; x<currentData.items[idx].choices.length; x++) {
      currentData.items[idx].choices[x].isCorrect = (x === cIdx);
    }
  } else {
    currentData.items[idx].choices[cIdx].isCorrect = val;
  }
  syncVisualToJson(); renderVisualEditor(currentData);
}
function addChoiceOption(idx) { currentData.items[idx].choices.push({ value: "新しい選択肢", isCorrect: false }); syncVisualToJson(); renderVisualEditor(currentData); }
function removeChoice(idx, cIdx) { currentData.items[idx].choices.splice(cIdx, 1); syncVisualToJson(); renderVisualEditor(currentData); }

function addNewItem(type) {
  var newItem = { type: type, title: type === 'section' ? "新しいセクション" : "新しい質問", required: false };
  if (['radio', 'checkbox', 'dropdown'].includes(type)) newItem.choices = [{ value: "選択肢1", isCorrect: false }];
  if (type === 'section') newItem.pageNavigation = "CONTINUE";
  if (currentData.isQuiz && type !== 'section') newItem.points = 10;
  currentData.items.push(newItem);
  syncVisualToJson(); renderVisualEditor(currentData);
}
function removeItem(idx) { currentData.items.splice(idx, 1); syncVisualToJson(); renderVisualEditor(currentData); }
function moveItem(idx, direction) {
  var targetIdx = idx + direction;
  if (targetIdx < 0 || targetIdx >= currentData.items.length) return;
  var temp = currentData.items[idx]; currentData.items[idx] = currentData.items[targetIdx]; currentData.items[targetIdx] = temp;
  syncVisualToJson(); renderVisualEditor(currentData);
}

function generateForm() {
  var btn = document.getElementById('btnCreate');
  var status = document.getElementById('status');
  var success = document.getElementById('successArea');
  btn.disabled = true; success.innerHTML = ''; status.style.display = 'block'; status.innerHTML = "Googleフォームを生成中...";
  google.script.run.withSuccessHandler(function(res) {
    btn.disabled = false;
    if (res.success) {
      currentFormId = res.id; status.style.display = 'none';
      success.innerHTML = `
        <div class="success-banner">
          <strong>フォームを作成しました。</strong><br><br>
          <a href="${res.editUrl}" target="_blank">🔗 編集画面を開く</a><br>
          <a href="${res.publishedUrl}" target="_blank">🔗 回答画面を開く</a><br>
        </div>`;
    } else { status.innerHTML = 'エラー: ' + res.message; }
  }).createFormFromJson(JSON.stringify(currentData));
}


</script>

</body>
</html>

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?