山羊の午後

研究関係の備忘録。

フロントエンド実装のスタート

前回 生成AIを使ってゼロからWebサービスをつくる - 山羊の午後

生成AIでゼロからWebサービスを作る、2日目。今日は実装を進めたい。
まずは初めてやるフロントエンドの実装からスタート。とりあえず、ChatGPTに何をしたらいいのか聞く。
UIについて検討しては、というので1日目に決めたMVPの機能リストから、UIのイメージを作ってもらう。
分からないので自分で考えたりはしない。

MVPの機能リストはこれ。(これは自分で決めた)
ちなみに作るものは「小説のワンシーンを出力する生成AIアプリ」である。

  • ユーザー入力を受け付けるフォームがある
  • 実行ボタンがある
  • 実行ボタンを押すとバックエンドで処理が実施される
  • 処理中はスピン画像がくるくるまわる
  • 処理結果が表示される
  • 実行ボタンが再実行の表示に変化する

そして生成された画像がこれ。

MVPの構成要素から生成されたUI

特に問題はないので、じゃあこれでMVPの実装をしてみて、とお願いする。
出てきたのがこちら。

import React, { useState } from 'react'

// スピナー表示コンポーネント
function Spinner() {
  return (
    <div className="flex justify-center mt-4">
      <div className="w-6 h-6 border-4 border-blue-500 border-dashed rounded-full animate-spin" />
    </div>
  )
}

type Inputs = {
  characterName: string
  perspective: 'first' | 'third'
  sceneType: '出会い' | '別れ' | 'クライマックス'
  instruction: string
}

export default function App() {
  const [inputs, setInputs] = useState<Inputs>({
    characterName: '',
    perspective: 'first',
    sceneType: 'クライマックス',
    instruction: ''
  })
  const [isLoading, setIsLoading] = useState(false)
  const [output, setOutput] = useState('')

  const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
    const { name, value } = e.target
    setInputs(prev => ({ ...prev, [name]: value }))
  }

  const handleGenerate = async () => {
    setIsLoading(true)
    setOutput('')
    await new Promise(r => setTimeout(r, 2000))
    setOutput(
      `${inputs.characterName} の「${inputs.sceneType}」シーン。` +
      `一人称? ${inputs.perspective === 'first' ? 'はい' : 'いいえ'}。` +
      `補足: ${inputs.instruction}`
    )
    setIsLoading(false)
  }

  return (
    <div className="min-h-screen bg-gray-100 flex items-center justify-center p-4">
      <div className="max-w-md w-full bg-white rounded-xl shadow p-6 space-y-6">
        <h1 className="text-2xl font-bold text-center">✨ 小説構想アプリ</h1>

        <div className="space-y-4">
          <div>
            <label className="block font-medium">主人公キャラクター名</label>
            <input
              type="text"
              name="characterName"
              value={inputs.characterName}
              onChange={handleChange}
              className="w-full p-2 border rounded focus:outline-blue-500"
            />
          </div>

          <div>
            <label className="block font-medium">視点</label>
            <div className="flex gap-4 mt-1">
              <label>
                <input
                  type="radio"
                  name="perspective"
                  value="first"
                  checked={inputs.perspective === 'first'}
                  onChange={handleChange}
                /> 一人称
              </label>
              <label>
                <input
                  type="radio"
                  name="perspective"
                  value="third"
                  checked={inputs.perspective === 'third'}
                  onChange={handleChange}
                /> 三人称
              </label>
            </div>
          </div>

          <div>
            <label className="block font-medium">展開</label>
            <select
              name="sceneType"
              value={inputs.sceneType}
              onChange={handleChange}
              className="w-full p-2 border rounded focus:outline-blue-500"
            >
              <option value="出会い">出会い</option>
              <option value="別れ">別れ</option>
              <option value="クライマックス">クライマックス</option>
            </select>
          </div>

          <div>
            <label className="block font-medium">補足指示(30字以内)</label>
            <input
              type="text"
              name="instruction"
              value={inputs.instruction}
              maxLength={30}
              onChange={handleChange}
              className="w-full p-2 border rounded focus:outline-blue-500"
            />
          </div>
        </div>

        <button
          onClick={handleGenerate}
          disabled={isLoading}
          className="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700 disabled:opacity-50"
        >
          {isLoading ? '生成中...' : output ? '再生成' : '生成開始'}
        </button>

        {isLoading && <Spinner />}

        {output && (
          <div className="mt-6 p-4 bg-gray-100 rounded shadow">
            <p className="whitespace-pre-wrap">{output}</p>
          </div>
        )}
      </div>
    </div>
  )
}

できた~。
割愛するが、一度の指示で完成したわけではなく、細かいところを修正しながら6回くらい生成させた。
しかし自分ではコードをまったく書いていない。
なのにちゃんと動くものになったのですばらしい。
つづいて、出来上がったコードの部品が何なのかを解説させて、Reactとはどんなものなのかを理解していく。
なんとなくわかった段階で、Reactの公式のチュートリアルを読む。
クイックスタート – React
UI の記述 – React
ふーん、と理解が進んだところで今日はここまで。