最近、AIでGoogleスライドなどを作成するツールが流行っていますよね。
そこで私も「自分でも作ってみよう!」と思ったのですが、難易度が高く挫折してしまいました(あたりまえ)。
だけど、「Googleフォームなら自分でもできるんじゃね?」と思って作ったのが、今回紹介するツールです。
🛠️ 使用環境
- GAS (Google Apps Script)
💡 仕組み
- AIにプロンプトを投げてJSONデータを作ってもらう
- 生成されたJSONをGASのウェブアプリに入力する
- 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
- このツールや改造コードを他人に販売したり、有料サービスに組み込んで利益を得る行為は禁止です。
⚠️ お約束(免責事項)
このツールを使って万が一データが消えたり、システムが止まったりしても責任は取れません。あくまで「自己責任」でのご利用をお願いいたします。
📖 使い方
- 「プロンプトをコピー」 ボタンを押します。
- コピーしたプロンプトをAIに投げます。
- ※プロンプトの最後に、どんなフォームを作りたいかの要望を記入してください。
- AIの指示に従ってJSONデータを生成させます。
- 作成されたJSONを入力欄に入れます(既存のテキストは消してください)。
- 必要に応じて、画面右側でJSONを編集することも可能です。
- 「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>
