
AIで要件からクリックできるプロトタイプを作る方法
見た目の整ったカードとボタンがあっても、予約確定で何も変わらなければ動作の検討はできません。要件からプロトタイプを作るときは画面だけでなく振る舞いを指定します。
原型で確かめる一つの判断を決める
架空例は午前・午後の予約で、各枠に最初は二席あります。確定で一席を使い、再確定で余計に減らさず、取り消すと元の枠へ一席戻します。
記憶するのはページ内だけです。再読み込みで初期化し、別タブと残席を共有しません。これはレビューの範囲で、本番の予約サーバーではありません。
| 状態 | 操作 | 期待結果 |
|---|---|---|
| 選択なし | 確定を試す | 確定不可、残席2/2 |
| 午前を選択 | 確定 | 午前1席、午前の予約 |
| 予約済み | 再確定 | 追加で減らない |
| 予約済み | 取り消し | 午前2席に戻り予約消去 |
| 予約なし | 再取り消し | 残席不変 |
| 午前満席 | Morning、続いてAfternoonを選択 | 午前は0のまま予約不可、午後は予約可能 |
| シナリオ変更 | 初期残席数を変更 | 予約と選択を解除し、選んだシナリオを初期化 |
「使いやすくする」より、作成側とレビュー側が同じ成功条件を持てます。
行動と実装範囲をAIへ伝える
予約フローの単一HTMLファイルを作ってください。
label/select/buttonと見える状態文を使います。
初期は午前2席、午後2席。ページにつき予約は一つ。
選択なしでは確定不可。選択枠に空席があるときだけ確定します。
予約中は再確定と選択変更を防ぎます。
取り消しは予約枠へ一席だけ戻して予約を消去し、再取消は何もしません。
更新はpoliteのlive regionで通知します。
外部ライブラリ、通信、個人情報、決済、永続化は含めません。
完全HTML、開き方、受け入れ例を返します。
同時予約や本番の保証はしません。状態表とプロンプトを送り、完全なHTMLを求めます。ファイルを開いて下の五つの受け入れ条件を操作し、画像や「実装済み」という回答だけで終えません。
EvoLinkへの依頼からレビュー用ファイルまで
- EvoLinkのGemini 4ページでAgentまたはAPIの接続手順を確認し、状態表とプロンプトを一緒に渡します。通常は空き枠2/2、午前満席は0/2とし、シナリオ変更時は選択と予約を初期化する条件を追加します。読みやすいレイアウト、残席数、予約・キャンセル・満席の明確な表示も指定します。
- スタイルとスクリプトを含む完全なHTMLを要求します。Agentには
booking.htmlとして保存させ、APIクライアントの場合は返されたHTML本文からMarkdownのコードフェンスを除いて保存します。JSONレスポンス全体をHTMLとして保存しないでください。開く前に、想定外の外部スクリプトや通信がないか確認します。 - ブラウザーで
booking.htmlを開きます。通常シナリオでMorningを選んで確定すると、残席が2から1になり、予約済みの表示が出ます。キャンセルすると2に戻り、選択が解除されます。テストでキャンセル処理を再実行しても2のままであることを確認します。 - Morning fullに切り替えてMorningを選びます。残席0、満席の案内、確定ボタンの無効化を確認します。Afternoonは予約でき、確定すると2から1になります。Normalに戻すと予約が消え、2/2に戻ります。この操作はテストデータの設定であり、他の利用者やサーバー側の同時処理を再現するものではありません。
- 失敗した場合は、初期状態、クリック手順、期待結果、実際の結果、HTMLを同じ接続経路で渡し、該当箇所の修正を依頼します。その後、両シナリオを再確認します。ゲートウェイ経由で別モデルと比較する前に、合格した出力と使用量を記録します。ファイルを生成できたことだけでは、品質やコストの優位性は判断できません。
実行できる完全な参考HTML
booking.htmlとして保存してブラウザーで開きます。以下は執筆した教材用の参考例であり、モデルの実測出力ではありません。実行ファイルを同一に保つため、全言語版で画面の表記は英語です。レイアウトと状態表示は動作レビューのためのもので、完成した製品デザインではありません。
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Session booking · Review prototype</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; background: #f4f6f8; color: #172033; font: 16px/1.6 system-ui, sans-serif; }
main { max-width: 640px; margin: 48px auto; padding: 28px; background: white; border: 1px solid #dce2ea; border-radius: 16px; }
h1 { margin: 8px 0; font-size: 30px; line-height: 1.2; }
p { margin: 12px 0; }
.eyebrow { color: #526077; font-size: 13px; text-transform: uppercase; letter-spacing: .08em; }
label { display: block; font-weight: 600; margin: 16px 0 6px; }
select, button { font: inherit; padding: 10px 14px; border: 1px solid #aeb9c8; border-radius: 8px; }
select { width: 100%; background: white; color: #172033; }
.actions { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 20px; }
button { cursor: pointer; background: white; color: #172033; }
#confirm { background: #193e87; color: white; border-color: #193e87; }
button:disabled { opacity: .5; cursor: not-allowed; }
:focus-visible { outline: 3px solid #b56b00; outline-offset: 3px; }
#status { background: #edf3fc; padding: 16px; border-radius: 8px; }
small { display: block; color: #526077; }
fieldset { margin: 28px 0 0; padding: 16px; border: 1px dashed #aeb9c8; border-radius: 8px; }
legend { font-weight: 600; }
@media (max-width: 680px) { main { margin: 16px; padding: 20px; } }
</style>
</head>
<body><main>
<p class="eyebrow">Interactive prototype · Local demo</p>
<h1>Book a workshop session</h1>
<p>Choose a session, confirm your place, or cancel to release it.</p>
<p id="capacity"></p>
<label for="slot">Session</label>
<select id="slot"><option value="">Choose a session</option>
<option value="am">Morning</option><option value="pm">Afternoon</option></select>
<div class="actions">
<button id="confirm" type="button" disabled>Confirm booking</button>
<button id="cancel" type="button" disabled>Cancel booking</button>
</div>
<p id="status" role="status" aria-live="polite"></p>
<small>One booking per page. Refresh resets this demo; other tabs do not share availability.</small>
<fieldset><legend>Review scenarios</legend>
<label for="scenario">Starting availability</label>
<select id="scenario"><option value="normal">Normal · 2 morning / 2 afternoon</option>
<option value="full">Morning full · 0 morning / 2 afternoon</option></select>
<p><small>Changing the scenario clears the current booking and selection. This control is for review only.</small></p>
</fieldset>
</main>
<script>
const capacity = { am: 2, pm: 2 };
const names = { am: "Morning", pm: "Afternoon" };
let selected = "";
let booking = null;
const slot = document.getElementById("slot");
const scenario = document.getElementById("scenario");
const confirmButton = document.getElementById("confirm");
const cancelButton = document.getElementById("cancel");
const status = document.getElementById("status");
const availability = document.getElementById("capacity");
function render(message = "") {
const valid = Object.hasOwn(capacity, selected);
confirmButton.disabled = !valid || booking !== null || capacity[selected] <= 0;
cancelButton.disabled = booking === null;
slot.disabled = booking !== null;
availability.textContent = `Available: morning ${capacity.am}, afternoon ${capacity.pm}`;
status.textContent = message || (booking !== null
? `Booked: ${names[booking]}. Your place is reserved in this demo.`
: valid && capacity[selected] === 0
? `${names[selected]} is full. Choose another session.`
: "Choose a session to book your place.");
}
slot.addEventListener("change", () => { selected = slot.value; render(); });
confirmButton.addEventListener("click", () => {
if (!Object.hasOwn(capacity, selected) || booking !== null || capacity[selected] <= 0) return;
capacity[selected] -= 1;
booking = selected;
render();
});
cancelButton.addEventListener("click", () => {
if (booking === null) return;
const cancelled = names[booking];
capacity[booking] += 1;
booking = null;
selected = "";
slot.value = "";
render(`${cancelled} booking cancelled. Your place has been released.`);
});
scenario.addEventListener("change", () => {
capacity.am = scenario.value === "full" ? 0 : 2;
capacity.pm = 2;
booking = null;
selected = "";
slot.value = "";
render("Review scenario reset. Choose a session to test it.");
});
render();
</script>
</body></html>見た目より先に動作をテストする
未選択で確定が無効か確かめ、午前を選び確定して予約表示を見ます。取り消して両枠二席へ戻ること、取り消しを繰り返して三席にならないことをUIか自動検査で調べます。
別の枠も試し、次の選択前に予約を消せるか確認します。無効ボタンだけに頼らずハンドラーにも条件が必要です。コードから再呼び出しされても正しい遷移を保つためです。
満席の分岐はReview scenariosから直接検証できます。通信失敗やリクエスト処理中の状態は、このローカル例の対象外です。実際のサービスにつなぐ前に別途定義してください。
HTMLだけでなく要件の不足も見つける
| 観察 | 決めること |
|---|---|
| 予約後に枠を変えたい | 取消して再予約か、独立した変更か |
| 二人が最後の席を予約 | どのサーバーが残席と競合を管理するか |
| 再読み込みで消える | 永続化も今回の範囲か |
| キーボードで使えない | ラベル、フォーカス順、通知は十分か |
次の実装では信頼できる残席と競合制御をバックエンドへ置きます。ローカルカウンターは同時予約の証明ではありません。
Next.jsへ組み込む際は既存のコンポーネントと状態境界を使い、単独ページをそのまま本番へ貼りません。HTMLは製品上の判断を具体化するレビュー資料です。
動くファイルと要件を一緒に引き渡す
要件版、ファイル、テスト表、観察結果、未解決の製品質問を残します。取消規則が変わったら状態表とUIを両方直し、影響した例を再確認します。
よくある質問
プロトタイプにバックエンドは必須ですか?
この局所フローには不要ですが、共有予約と永続的残席には権威ある実装が必要です。
デザイン以外に何を渡しますか?
状態、操作、期待結果、失敗時、範囲です。画像だけでは遷移を定義できません。
ボタンを無効にすれば十分ですか?
処理側でも条件を検証します。UI状態だけを防御にしません。
取消を二回試す理由は?
一つの予約で複数回席を戻す不具合を見つけるためです。
本番へそのまま使えますか?
実アプリの構造に合わせ、バックエンド、検証、アクセシビリティを補います。
レビューできる条件は?
開けて約束した操作が動き、制限を引き継ぎ、評価者がテストを再現できることです。


