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?

HTML/CSS/JavaScriptで画像生成API向けの入力補助UIを作る

0
Last updated at Posted at 2026-06-11

はじめに

note や SNS に投稿するためのアイキャッチ画像を作りたいだけなら、必ずしも画像生成AIつきの月額サービスを追加契約する必要はありません。

たとえば、普段は Claude で文章作成や記事構成を考えている人が、投稿用の画像だけを必要なときに生成したい場合があります。

Claude は画像生成プロンプトを作ることはできますが、一般的な画像生成ツールのように写真やイラストを直接生成する用途ではありません。

そこで考えられるのが、次のような分業です。

Claude:
  記事内容の整理
  アイキャッチ画像のコンセプト作成
  画像生成プロンプト作成
  失敗した画像への改善指示作成

画像生成API:
  実際の画像生成

自作UI:
  プロンプト、モデル、サイズ、参照画像、履歴を扱う入力補助画面

この記事では、SNS投稿用や note のアイキャッチ画像作成を題材に、画像生成API向けの小さな入力補助UIを HTML / CSS / JavaScript で作ります。

既存のAIサービスや画像生成APIを補助するための小さな画面を作る、というケーススタディです。

完成版はこちらです。

👉 CodePen:

Chrome ブラウザーで HTML ファイルの表示を確認したい場合、Web Maker を導入する選択肢があります。


想定読者

この記事は、次のような人を想定しています。

- Claude など、画像生成機能を持たない文章AIを主に使っている人
- note / Qiita / Zenn / X / Threads などに投稿している人
- 投稿用アイキャッチ画像をたまに作りたい人
- 画像生成AIの月額契約までは迷っている人
- fal.ai などの画像生成APIを試したい人
- Chrome拡張やサイドパネルUIに興味がある人
- HTML / CSS / JavaScript のUI練習題材を探している人

なぜ作るのか

今回の主題は「画像生成APIを使おう」ではありません。

むしろ、問いは次です。

SNS投稿用の画像を作るためだけに、
画像生成AIつきの月額サービスを追加契約する必要はあるのか?

画像生成を毎日大量に行うなら、専用サービスの月額契約は便利です。

しかし、note のアイキャッチ画像や SNS 投稿用画像のように、必要なときに数枚試して1枚使えればよい用途もあります。

このような用途では、次のような構成が現実的です。

文章作成:
  Claude などの文章AI

画像生成:
  fal.ai などの画像生成API

入力補助:
  自分用の小さなWeb UI

既存サービスの入力画面を補助するための、小さなアプリです。


選択肢を比較する

画像生成のための選択肢を大きく分けると、次のようになります。

選択肢 向いている用途 注意点
画像生成つきAIサービス 対話的な調整、本命画像の作成、継続的な画像制作 月額費用が用途に対して重い場合がある
Claudeなど文章AI + 画像生成API 文章AIは使っていて、画像だけ必要なときに生成したい APIキー管理と簡単な実装が必要
既存の無料画像生成サービス 少量の試用、品質確認 上限や履歴管理に制約がある
AIエージェント + 画像/動画API 複数工程の自動化、連続加工、動画生成 設計とコストが重くなりやすい

APIのほうが常に安い、という話ではありません。

ただし、利用頻度が低い場合や、SNS投稿用の単発生成が中心の場合は、月額契約の前に API 利用と小さな入力UIで十分かどうかを試す価値があります。


今回作るもの

今回作るのは、画像生成サンドボックス風の小さなUIです。

固定フォーム
├── モード切替
│   ├── テキストから画像
│   └── 画像から画像
├── モデル選択
├── サイズ選択
└── 入力行
    ├── 参照画像サムネイル
    ├── プロンプト
    └── 生成ボタン

履歴エリア
└── 生成中カード / 完成カード

画面の主な機能は次です。

- 上部に入力フォームを固定する
- プロンプト送信で履歴カードを追加する
- 生成中カードを先に表示する
- setTimeoutで疑似的に生成完了にする
- テキストから画像 / 画像から画像を切り替える
- モードごとにモデル選択肢を切り替える
- 画像から画像モードだけ参照画像を選べるようにする
- 参照画像を小さなサムネイルとして表示する

この記事では、まずAPIなしでUIを作ります。

そのあと、API接続、ポーリング、履歴ストレージの考え方を整理します。


1. 上部に入力フォームを固定する

まずは、入力フォームを画面上部に固定します。

<header class="generator-header">
  <form class="generator-form">
    <input placeholder="プロンプトを入力">
    <button>生成</button>
  </form>
</header>

<main class="gallery">
  <p>ここに生成履歴を表示します</p>
</main>
body {
  margin: 0;
  padding-top: 120px;
  font-family: system-ui, sans-serif;
  background: #f5f5f5;
}

.generator-header {
  position: fixed;
  inset: 0 0 auto;
  z-index: 10;
  padding: 16px;
  background: white;
  border-bottom: 1px solid #ddd;
}

.generator-form {
  max-width: 960px;
  margin: 0 auto;
  display: flex;
  gap: 8px;
}

.gallery {
  max-width: 960px;
  margin: 0 auto;
  padding: 16px;
}

ポイントは、position: fixed と body 側の padding-top です。

フォームを固定すると、その下のコンテンツがフォームの裏に隠れます。そのため、body 側に上余白を入れます。


2. submitイベントで履歴カードを追加する

次に、フォーム送信時に履歴カードを追加します。

<form id="generatorForm">
  <input id="promptInput" placeholder="プロンプトを入力">
  <button>生成</button>
</form>

<main id="gallery"></main>
const form = document.querySelector("#generatorForm");
const promptInput = document.querySelector("#promptInput");
const gallery = document.querySelector("#gallery");

let count = 0;

form.addEventListener("submit", (event) => {
  event.preventDefault();

  const prompt = promptInput.value.trim();

  if (!prompt) {
    return;
  }

  count += 1;

  const card = document.createElement("article");
  card.textContent = `#${count}: ${prompt}`;

  gallery.prepend(card);
  promptInput.value = "";
});

gallery.prepend(card) を使うと、新しい履歴が上に追加されます。

画像生成UIでは、新しい生成結果を上に積むほうが見やすいことが多いです。


3. 生成中カードを先に表示する

画像生成では、結果がすぐ返るとは限りません。

そこで、先に「生成中カード」を表示します。

function createLoadingCard(prompt) {
  const card = document.createElement("article");
  card.className = "image-card is-loading";

  card.innerHTML = `
    <div class="image-placeholder">生成中...</div>
    <p>${prompt}</p>
    <span class="status">生成中</span>
  `;

  return card;
}

CSSは最小限ならこうです。

.image-card {
  display: grid;
  gap: 12px;
  padding: 12px;
  border: 1px solid #ddd;
  border-radius: 16px;
  background: white;
}

.image-placeholder {
  aspect-ratio: 1 / 1;
  display: grid;
  place-items: center;
  background: #eee;
  border-radius: 12px;
}

.status {
  width: fit-content;
  padding: 4px 8px;
  border-radius: 999px;
  background: #fff7d6;
  color: #806000;
  font-size: 12px;
}

4. setTimeoutで疑似的に生成完了にする

最初から実APIにつなぐと、APIキー管理やポーリングの話が混ざります。

まずは setTimeout() で、3秒後に生成完了したことにします。

function completeCard(card) {
  card.classList.remove("is-loading");
  card.classList.add("is-complete");

  const image = card.querySelector(".image-placeholder");
  const status = card.querySelector(".status");

  image.textContent = "IMAGE";
  image.className = "generated-image";
  status.textContent = "生成完了";
}
const card = createLoadingCard(prompt);
gallery.prepend(card);

setTimeout(() => {
  completeCard(card);
}, 3000);

完成画像のダミー表示です。

.generated-image {
  aspect-ratio: 1 / 1;
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, #c7d2fe, #fbcfe8);
  border-radius: 12px;
}

この段階では、APIを呼んでいません。

後で、この setTimeout() の部分を fetch() やポーリングに置き換えます。


5. テキストから画像 / 画像から画像のモードを切り替える

次に、画像生成モードを追加します。

<label>
  <input type="radio" name="mode" value="text-to-image" checked>
  テキストから画像
</label>

<label>
  <input type="radio" name="mode" value="image-to-image">
  画像から画像
</label>

選択中のモードを取得します。

function getSelectedMode() {
  return document.querySelector("input[name='mode']:checked").value;
}

送信時に、プロンプトだけでなくモードも扱います。

const mode = getSelectedMode();

const request = {
  mode,
  prompt
};

このあたりから、フォーム入力を直接バラバラに扱うのではなく、request オブジェクトとしてまとめると扱いやすくなります。


6. モードごとにモデル選択肢を切り替える

fal.ai のサンドボックス風に、モードごとにモデル候補を切り替えます。

<select id="modelSelect"></select>
const modelSelect = document.querySelector("#modelSelect");

const modelOptionsByMode = {
  "text-to-image": [
    { value: "flux-schnell", label: "FLUX Schnell" },
    { value: "flux-dev", label: "FLUX Dev" }
  ],
  "image-to-image": [
    { value: "flux-kontext", label: "FLUX Kontext" },
    { value: "image-edit-basic", label: "Image Edit Basic" }
  ]
};

select の中身を作り直します。

function renderModelOptions(mode) {
  modelSelect.replaceChildren();

  for (const option of modelOptionsByMode[mode]) {
    const optionElement = document.createElement("option");
    optionElement.value = option.value;
    optionElement.textContent = option.label;
    modelSelect.append(optionElement);
  }
}

モード変更時に呼び出します。

const modeInputs = document.querySelectorAll("input[name='mode']");

for (const modeInput of modeInputs) {
  modeInput.addEventListener("change", () => {
    renderModelOptions(modeInput.value);
  });
}

renderModelOptions("text-to-image");

replaceChildren() を使うと、既存の選択肢を一度消してから作り直せます。


7. 画像から画像モードだけ参照画像ボタンを表示する

画像から画像モードでは、参照画像が必要です。

ただし、標準の file input をそのまま見せると、画面が大きくなりがちです。

そこで、小さなサムネイルボタンとして扱います。

<label class="source-thumb-button" id="sourceThumbButton">
  <input
    id="sourceImageInput"
    type="file"
    accept="image/png, image/jpeg, image/webp"
  >
  <img id="sourcePreview" alt="参照画像プレビュー">
  <span class="source-placeholder">+</span>
</label>
.source-thumb-button {
  display: none;
  width: 52px;
  height: 52px;
  border: 1px solid #ccc;
  border-radius: 14px;
  background: white;
  overflow: hidden;
  cursor: pointer;
  place-items: center;
}

.is-image-to-image .source-thumb-button {
  display: grid;
}

.source-thumb-button input {
  display: none;
}

.source-thumb-button img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.source-thumb-button img:not([src]) {
  display: none;
}

.source-placeholder {
  font-size: 24px;
  color: #777;
}

フォーム全体に is-image-to-image クラスがついているときだけ表示します。

function setupMode(mode) {
  form.classList.toggle("is-text-to-image", mode === "text-to-image");
  form.classList.toggle("is-image-to-image", mode === "image-to-image");

  renderModelOptions(mode);
}

参照画像は、生成結果カードには表示しません。

今回の用途では、フォーム上で「参照画像を選択済みか」を確認できれば十分だからです。


8. URL.createObjectURLで参照画像をプレビューする

選択した画像を、送信前に小さく確認できるようにします。

const sourceImageInput = document.querySelector("#sourceImageInput");
const sourcePreview = document.querySelector("#sourcePreview");
const sourceThumbButton = document.querySelector("#sourceThumbButton");

let sourceImagePreviewUrl = "";

sourceImageInput.addEventListener("change", () => {
  const file = sourceImageInput.files[0];

  if (!file) {
    return;
  }

  if (sourceImagePreviewUrl) {
    URL.revokeObjectURL(sourceImagePreviewUrl);
  }

  sourceImagePreviewUrl = URL.createObjectURL(file);
  sourcePreview.src = sourceImagePreviewUrl;
  sourceThumbButton.classList.add("has-image");
});

画像を選択済みの場合、+ を消します。

.source-thumb-button.has-image .source-placeholder {
  display: none;
}

注意点があります。

URL.createObjectURL() で作られる blob: URL は、ブラウザ内の一時URLです。

外部の画像生成APIが直接アクセスできるURLではありません。

APIに参照画像を送る場合は、画像ファイルを FormData で送るか、いったんアップロードして外部からアクセスできるURLにする必要があります。


9. requestオブジェクトとして入力内容をまとめる

フォームの値を、1回の生成リクエストとしてまとめます。

const modeLabels = {
  "text-to-image": "テキストから画像",
  "image-to-image": "画像から画像"
};

const sizeLabels = {
  square: "正方形 1:1",
  landscape: "横長 16:9",
  portrait: "縦長 9:16"
};
const request = {
  id: imageCount,
  mode,
  modeLabel: modeLabels[mode],
  prompt,
  model,
  modelLabel,
  size,
  sizeLabel: sizeLabels[size],
  status: "loading"
};

if (mode === "image-to-image") {
  request.sourceImage = {
    file: sourceImageInput.files[0]
  };
}

この形にしておくと、次の処理を同じ情報から作れます。

- 履歴カード表示
- API送信
- ローカル履歴保存
- エラー表示

DOMの状態を直接あちこちで参照し続けるより、いったん request オブジェクトにまとめるほうが見通しがよくなります。


10. fetch + FormDataで参照画像を送る考え方

画像から画像モードでは、参照画像ファイルを送る必要があります。

画像ファイルは JSON ではなく、FormData で送るのが基本です。

const formData = new FormData();

formData.append("image", request.sourceImage.file);
formData.append("prompt", request.prompt);
formData.append("model", request.model);
formData.append("size", request.size);

await fetch("/api/generate-image-to-image", {
  method: "POST",
  body: formData
});

FormData を使う場合、通常は Content-Type を自分で指定しません。

// 基本的には書かない
headers: {
  "Content-Type": "multipart/form-data"
}

ブラウザが multipart/form-data の boundary を自動で付けるためです。

また、実用時はブラウザから fal.ai などのAPIキーを直接使わないほうが安全です。

構成としては、次のようにします。

ブラウザ
  ↓
自分のバックエンド / Cloudflare Workers
  ↓
画像生成API

フロントエンドからは自分の /api/generate-image-to-image に送ります。

APIキーはバックエンドや Workers の環境変数として持たせます。


11. fal.ai などのAPIではアップロードと生成を分けて考える

画像生成APIに参照画像を渡す場合、概念的には次の2段階になります。

1. 参照画像ファイルをアップロードする
2. アップロード後のURLをモデル入力に渡す

fal.ai のJavaScriptクライアントを使う場合は、ファイルアップロード用の仕組みや、キュー実行の仕組みがあります。

概念的には次のような流れです。

const imageUrl = await uploadImage(file);

const result = await runImageToImageModel({
  prompt,
  image_url: imageUrl,
  image_size: size
});

重要なのは、ブラウザ内の blob: URL をそのままモデル入力に渡しても、外部APIからは参照できないことです。

外部APIに渡すには、API側から取得できるURL、またはAPIに直接送信したファイルデータが必要です。


12. 実APIではポーリングで生成完了を待つ

ここまでのUIでは、setTimeout() で3秒後に生成完了にしていました。

実際のAPIでは、生成完了を確認する必要があります。

流れはこうです。

submit
↓
request_id を受け取る
↓
status を定期的に確認する
↓
completed なら result を取得する
↓
生成中カードを完成カードに差し替える

疑似コードです。

async function pollGenerationResult(requestId) {
  const maxAttempts = 60;

  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
    const status = await fetchGenerationStatus(requestId);

    if (status.state === "completed") {
      return status.result;
    }

    if (status.state === "failed") {
      throw new Error("画像生成に失敗しました。");
    }

    await sleep(2000);
  }

  throw new Error("画像生成がタイムアウトしました。");
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

setInterval() よりも、for ループと await sleep() のほうが説明しやすいです。

前のステータス確認が終わってから次の確認に進むためです。

UI側では、次のように置き換えます。

const card = createLoadingCard(request);
gallery.prepend(card);

try {
  const result = await submitAndPoll(request);

  completeCard({
    card,
    request,
    imageUrl: result.imageUrl
  });
} catch (error) {
  markCardAsError(card, error);
}

実装時には、必ずタイムアウトや最大試行回数を入れます。

無限にポーリングし続けるUIは避けます。


13. APIキーをどこに置くか

APIを使う場合、APIキーの扱いが重要です。

通常のWebアプリでは、ブラウザにAPIキーを直接置かない構成が基本です。

避けたい構成:
  ブラウザ
    ↓ APIキーつき fetch
  画像生成API

望ましい構成:
  ブラウザ
    ↓
  自分のバックエンド / Cloudflare Workers
    ↓ APIキーつき fetch
  画像生成API

公開するWebアプリなら、APIキーはサーバー側や Cloudflare Workers 側の環境変数で管理します。

一方、個人用の Chrome 拡張機能として作るなら、ユーザー自身のAPIキーを chrome.storage.local に保存する構成も考えられます。

Chrome拡張機能:
  options page:
    ユーザーがAPIキーを入力

  chrome.storage.local:
    APIキーを保存

  background service worker:
    APIキーを読み出してAPIを呼ぶ

ただし、chrome.storage.local は「完全に安全な金庫」ではありません。

ユーザー本人の端末上に保存するだけです。

公開配布する拡張機能に、開発者自身のAPIキーを埋め込む用途には向きません。


14. 履歴をどこに保存するか

今回のUIでは、履歴も考える必要があります。

保存したい情報は、大きく2種類に分けられます。

軽いデータ:
- プロンプト
- モード
- モデル
- サイズ
- 作成日時
- 生成画像URL
- ステータス

重いデータ:
- 参照画像ファイル
- 生成画像ファイル
- サムネイル画像

最初は、画像ファイルそのものを保存しないほうが簡単です。

まずは軽い履歴だけ保存します。

const historyItem = {
  id: crypto.randomUUID(),
  prompt: request.prompt,
  mode: request.mode,
  model: request.model,
  size: request.size,
  imageUrl: result.imageUrl,
  createdAt: new Date().toISOString()
};

Chrome拡張機能なら、軽い履歴は chrome.storage.local に保存できます。

async function saveHistoryItem(item) {
  const result = await chrome.storage.local.get("generationHistory");
  const history = result.generationHistory ?? [];

  const nextHistory = [item, ...history].slice(0, 100);

  await chrome.storage.local.set({
    generationHistory: nextHistory
  });
}

最新100件だけ残すようにしておくと、履歴が無限に増えるのを防げます。

const nextHistory = [item, ...history].slice(0, 100);

通常のWebアプリやPWAなら、chrome.storage.local は使えません。

その場合は、次のように考えます。

保存先 向いているもの
localStorage 軽い設定、少量の履歴
IndexedDB 画像Blob、サムネイル、大きめの履歴
Cloudflare R2 / S3 採用画像の長期保存
サーバー側DB 複数端末同期、検索、ユーザー管理

今回のチュートリアルでは、まず履歴は画面上だけで扱い、発展としてストレージ保存を考えるのがよいです。


15. 画像本体は保存するべきか

画像生成履歴を全部保存しようとすると、すぐに容量が大きくなります。

そのため、次のように分けると現実的です。

試行錯誤の画像:
  一時的なURLだけで確認する
  一定件数で消す

採用した画像:
  ダウンロードする
  または R2 / S3 / Google Drive などに保存する

長期的に検索したい履歴:
  プロンプト、モデル、サイズ、採用画像URLだけ保存する

SNS投稿や note のアイキャッチ画像では、すべての失敗画像を長期保存する必要はありません。

採用した画像だけを残せば十分なことも多いです。


16. PWAやChrome拡張への発展

今回のUIは、まず1枚HTMLとして作れます。

ただし、考え方は次の形に発展できます。

通常Webアプリ:
  URLで開いて使う

PWA:
  スマホのホーム画面に追加して使う

Chrome拡張:
  PCブラウザのサイドパネルやポップアップとして使う

スマホで使いたいなら、PWA化が候補になります。

PCで note や Claude の横に置きたいなら、Chrome拡張のサイドパネルが候補になります。

どちらの場合も、基本となるUI部品は同じです。

- プロンプト入力
- モード切替
- モデル選択
- サイズ選択
- 参照画像添付
- 生成中表示
- 履歴表示

違うのは、APIキーや履歴をどこに保存するかです。

Chrome拡張:
  chrome.storage.local

通常Webアプリ / PWA:
  localStorage / IndexedDB / サーバー側保存

まとめ

SNS投稿や note のアイキャッチ画像のためだけなら、画像生成AIつきの月額サービスを追加契約しなくてもよい場合があります。

Claude のような文章AIで画像生成プロンプトを作り、画像生成だけを fal.ai などのAPIに任せる構成でも十分なことがあります。

今回作ったUIは、そのための小さな入力補助画面です。

技術的には、次のようなWeb UIの基本を組み合わせています。

- fixed header
- submitイベント
- DOM追加
- setTimeoutによる疑似非同期処理
- radioによるモード切替
- selectの動的更新
- file input
- URL.createObjectURL
- FormData
- fetch
- polling
- storage

大きな制作環境を作る前に、まずは自分の作業に足りない部分だけを補う小さな画面を作る。

それが、今回のケーススタディです。


発展メモ

実用化するなら、次の順番で進めるのがよさそうです。

第1段階:
  1枚HTMLでUIだけ作る

第2段階:
  setTimeoutで疑似的な生成中UIを確認する

第3段階:
  自分のAPIエンドポイントにfetchする

第4段階:
  ポーリングで生成完了を待つ

第5段階:
  プロンプト、モデル、サイズ、画像URLを履歴保存する

第6段階:
  Chrome拡張、PWA、Cloudflare Workersなどに展開する

最初から全部作る必要はありません。

まずは、画像生成AIの標準UIを置き換えるのではなく、自分の投稿作業を補助する小さなUIとして考えるのがよいと思います。

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?