フロントエンド実装のスタート
前回 生成AIを使ってゼロからWebサービスをつくる - 山羊の午後
生成AIでゼロからWebサービスを作る、2日目。今日は実装を進めたい。
まずは初めてやるフロントエンドの実装からスタート。とりあえず、ChatGPTに何をしたらいいのか聞く。
UIについて検討しては、というので1日目に決めたMVPの機能リストから、UIのイメージを作ってもらう。
分からないので自分で考えたりはしない。
MVPの機能リストはこれ。(これは自分で決めた)
ちなみに作るものは「小説のワンシーンを出力する生成AIアプリ」である。
- ユーザー入力を受け付けるフォームがある
- 実行ボタンがある
- 実行ボタンを押すとバックエンドで処理が実施される
- 処理中はスピン画像がくるくるまわる
- 処理結果が表示される
- 実行ボタンが再実行の表示に変化する
そして生成された画像がこれ。

特に問題はないので、じゃあこれで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
ふーん、と理解が進んだところで今日はここまで。