はじめに
手っ取り早くTODOアプリのUIを作る必要があったので、AIにコードを書いてもらうことにしました。調べてみるとVercel社のv0というAIツールがUIに特化しているという情報があったので試してみました。
v0の概要
v0とは
Next.jsの開発元であるVercel社が提供するWebデザインとコード生成に特化したAIツールです。以下の画面からプロンプトを実行することでコードを生成することができます。
https://v0.dev/chat
PublicBetaなので、まだ開発段階のツールとなります。この後、機能など変更される可能性があります。
どんなコードが生成されるか
- 生成されるコードは、Reactで出力されます。
- UIコンポーネントは、shadcn/uiが使われます。
- リンク、画像、フォントについては、Next.js Link、Next.js Image、およびNext.js Fontが使われます。
生成したコードをどう活用するか
- 生成したコードをコピーして、自身のプロジェクトで利用できます。
- または生成したコードをベースにCLIでNext.jsのプロジェクトを作ることができます。
「Chat」と「Legacy」
v0には、新しくリリースされた「Chat」と1つ前の「Legacy」が存在します。今回は「Chat」を使っていきます。
- Chat:https://v0.dev/chat
- Legacy:https://v0.dev/?from=chat
当記事内でv0という単語は度々登場しますが、「Chat」の方を指します。
料金
以下の3プランが用意されています。今回はFreeプランを使っていきます。
(FreeプランでChatをどのくらい使えるのか、ドキュメントからは読み取れず、あまり分かってないです...)
公式ドキュメント
以下が公式のドキュメントになりますので、詳細はそちらをご覧ください。
https://v0.dev/docs
そのほか
実際にTODOアプリ作ってみる
ここからは実際にv0にTODOアプリを作ってもらいます。
流すプロンプト
以下のプロンプトを流します。
ReactでTODOアプリを作りたいのでソースコードを書いてください。
要件は以下になります。
・開発言語はTypeScript
・必要な画面は1枚
・TODOの作成 / TODOの削除 / TODOの更新 / TODOの参照が出来る画面が必要
・バックエンドとの通信はRestAPIで行う
・TODOのデータ自体は、RestAPIで取得する
プロンプトの実行結果
実行結果は以下になります。期待通りTodoアプリのコードを生成してくれました。
v0の回答
はい、TypeScriptを使用したReactでTODOアプリを作成しましょう。以下の要件に基づいて、1つのコンポーネントにTODOの作成、削除、更新、参照の機能を実装します。また、バックエンドとの通信はREST APIを使用します。
それでは、TypeScriptを使用したReactコンポーネントを作成します。
import React, { useState, useEffect } from 'react';
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Checkbox } from "@/components/ui/checkbox"
interface Todo {
id: number;
title: string;
completed: boolean;
}
const API_BASE_URL = 'https://api.example.com/todos'; // APIのベースURLを適切に設定してください
export default function TodoApp() {
const [todos, setTodos] = useState<Todo[]>([]);
const [newTodo, setNewTodo] = useState('');
const [editingTodo, setEditingTodo] = useState<Todo | null>(null);
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = async () => {
try {
const response = await fetch(API_BASE_URL);
const data = await response.json();
setTodos(data);
} catch (error) {
console.error('Error fetching todos:', error);
}
};
const addTodo = async () => {
if (!newTodo.trim()) return;
try {
const response = await fetch(API_BASE_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: newTodo, completed: false }),
});
const data = await response.json();
setTodos([...todos, data]);
setNewTodo('');
} catch (error) {
console.error('Error adding todo:', error);
}
};
const updateTodo = async (todo: Todo) => {
try {
const response = await fetch(`${API_BASE_URL}/${todo.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(todo),
});
const updatedTodo = await response.json();
setTodos(todos.map(t => t.id === updatedTodo.id ? updatedTodo : t));
setEditingTodo(null);
} catch (error) {
console.error('Error updating todo:', error);
}
};
const deleteTodo = async (id: number) => {
try {
await fetch(`${API_BASE_URL}/${id}`, { method: 'DELETE' });
setTodos(todos.filter(todo => todo.id !== id));
} catch (error) {
console.error('Error deleting todo:', error);
}
};
const toggleComplete = (todo: Todo) => {
updateTodo({ ...todo, completed: !todo.completed });
};
return (
<div className="max-w-md mx-auto mt-10 p-6 bg-white rounded-lg shadow-xl">
<h1 className="text-2xl font-bold mb-4">TODO App</h1>
<div className="flex mb-4">
<Input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="新しいTODOを入力"
className="flex-grow mr-2"
/>
<Button onClick={addTodo}>追加</Button>
</div>
<ul className="space-y-2">
{todos.map(todo => (
<li key={todo.id} className="flex items-center justify-between bg-gray-100 p-2 rounded">
{editingTodo?.id === todo.id ? (
<Input
type="text"
value={editingTodo.title}
onChange={(e) => setEditingTodo({ ...editingTodo, title: e.target.value })}
className="flex-grow mr-2"
/>
) : (
<div className="flex items-center">
<Checkbox
checked={todo.completed}
onCheckedChange={() => toggleComplete(todo)}
className="mr-2"
/>
<span className={todo.completed ? 'line-through' : ''}>{todo.title}</span>
</div>
)}
<div>
{editingTodo?.id === todo.id ? (
<Button onClick={() => updateTodo(editingTodo)} className="mr-2">保存</Button>
) : (
<Button onClick={() => setEditingTodo(todo)} className="mr-2">編集</Button>
)}
<Button onClick={() => deleteTodo(todo.id)} variant="destructive">削除</Button>
</div>
</li>
))}
</ul>
</div>
);
}
このコンポーネントは以下の機能を提供します:
- TODOの作成: 新しいTODOをテキスト入力し、「追加」ボタンをクリックして作成します。
- TODOの削除: 各TODOの「削除」ボタンをクリックして削除します。
- TODOの更新: 「編集」ボタンをクリックしてTODOを編集モードにし、変更後に「保存」ボタンをクリックして更新します。
- TODOの参照: コンポーネントがマウントされた時点で、すべてのTODOをAPIから取得して表示します。
- TODOの完了状態の切り替え: チェックボックスをクリックしてTODOの完了状態を切り替えます。
注意点:
-
API_BASE_URL
を実際のAPIエンドポイントに置き換える必要があります。 - エラーハンドリングは基本的なものしか実装していないので、実際の使用時にはより堅牢なエラーハンドリングを追加することをお勧めします。
- このコンポーネントは基本的な機能を提供していますが、実際のアプリケーションではさらにスタイリングやエラー表示、ローディング状態の表示などを追加することで、ユーザー体験を向上させることができます。
このコードをベースに、必要に応じて機能を追加したり、スタイリングをカスタマイズしたりすることができます。
ローカルにNext.js環境を作成
次にローカルで画面を立ち上げて、TODOアプリを確認したいので環境を作ります。
v0では、CLIから生成したコードをベースとしたNext.jsのプロジェクトを立ち上げる機能を提供しています。
画面右上から「Add to Codebase」を開きます。
CLIコマンドが表示されるのでコピーします。
CLIコマンドを実行して、プロジェクトを作成します。
npx shadcn@latest add "https://v0.dev/chat/b/b_vowbOz3fa72?token=abcde12345"
結果の確認
Next.jsでローカル実行すると、先ほどのTODOアプリが表示されます。
以下は、左の画像がTODOアプリのTODOが空の状態で、右の画像が試しにTODOを追加して表示した状態となります。
初期表示 | TODOを表示 |
---|---|
これだけで簡単にUIを作れちゃいました。
番外編:Claude 3.5 SonetでTODOアプリを生成
ここでふと他の生成AIでも別によくね?と思いまして、Claude 3.5 Sonetで同じプロンプトを試しました。結果としては、やはりv0の方がリッチな画面を作ってくれます。
実行結果
生成AI(Claude 3.5 Sonet)に同じプロンプトを流すと、以下のようになります。
初期表示 | TODOを表示 |
---|---|
AIの回答
はい、TypeScriptを使用したReactのTODOアプリケーションのソースコードを作成します。以下の要件に基づいて、1つの画面でTODOの作成、削除、更新、参照ができるコンポーネントを作成します。また、バックエンドとの通信はREST APIを使用して行います。
import React, { useState, useEffect } from 'react';
import axios from 'axios';
interface Todo {
id: number;
title: string;
completed: boolean;
}
const TodoApp: React.FC = () => {
const [todos, setTodos] = useState<Todo[]>([]);
const [newTodo, setNewTodo] = useState('');
const [editingTodo, setEditingTodo] = useState<Todo | null>(null);
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = async () => {
try {
const response = await axios.get<Todo[]>('https://api.example.com/todos');
setTodos(response.data);
} catch (error) {
console.error('Failed to fetch todos:', error);
}
};
const addTodo = async () => {
if (!newTodo.trim()) return;
try {
const response = await axios.post<Todo>('https://api.example.com/todos', {
title: newTodo,
completed: false,
});
setTodos([...todos, response.data]);
setNewTodo('');
} catch (error) {
console.error('Failed to add todo:', error);
}
};
const deleteTodo = async (id: number) => {
try {
await axios.delete(`https://api.example.com/todos/${id}`);
setTodos(todos.filter(todo => todo.id !== id));
} catch (error) {
console.error('Failed to delete todo:', error);
}
};
const updateTodo = async (id: number, title: string, completed: boolean) => {
try {
const response = await axios.put<Todo>(`https://api.example.com/todos/${id}`, {
title,
completed,
});
setTodos(todos.map(todo => todo.id === id ? response.data : todo));
setEditingTodo(null);
} catch (error) {
console.error('Failed to update todo:', error);
}
};
const startEditing = (todo: Todo) => {
setEditingTodo(todo);
};
return (
<div>
<h1>TODO App</h1>
<input
type="text"
value={newTodo}
onChange={(e) => setNewTodo(e.target.value)}
placeholder="New todo"
/>
<button onClick={addTodo}>Add Todo</button>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{editingTodo && editingTodo.id === todo.id ? (
<>
<input
type="text"
value={editingTodo.title}
onChange={(e) => setEditingTodo({ ...editingTodo, title: e.target.value })}
/>
<button onClick={() => updateTodo(todo.id, editingTodo.title, todo.completed)}>
Save
</button>
</>
) : (
<>
<span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
{todo.title}
</span>
<input
type="checkbox"
checked={todo.completed}
onChange={() => updateTodo(todo.id, todo.title, !todo.completed)}
/>
<button onClick={() => startEditing(todo)}>Edit</button>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</>
)}
</li>
))}
</ul>
</div>
);
};
export default TodoApp;
APIの作成
先ほどv0に生成してもらったコードは、API呼んでサーバからTODOを取得したり、編集したりするロジック周りも含まれています。
とすると、APIも欲しくなってきます。
ということでv0にAPIも作ってもらいます。今回は、OpenAPIで定義してもらうこ都にしました。
OpenAPIは、APIを定義するための共通的なインターフェース記述言語の仕様です。
https://ja.wikipedia.org/wiki/OpenAPI_Specification
流すプロンプト
上記のTODOアプリに使用するAPIについて、opneapi.ymlとして出力してもらえますか?
実行結果
v0の回答
はい、もちろんです。TODOアプリ用のREST APIを定義するOpenAPI仕様(YAML形式)を作成します。この仕様は、先ほど実装したTODOアプリの機能に合わせて設計されています。
以下が、TODOアプリ用のOpenAPI仕様(YAML形式)です:
openapi: 3.0.0
info:
title: TODO API
version: 1.0.0
description: A simple API for managing TODOs
servers:
- url: https://api.example.com/v1
paths:
/todos:
get:
summary: List all TODOs
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Todo'
post:
summary: Create a new TODO
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TodoCreate'
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
'400':
$ref: '#/components/responses/BadRequest'
/todos/{todoId}:
get:
summary: Get a specific TODO
parameters:
- name: todoId
in: path
required: true
schema:
type: integer
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
'404':
$ref: '#/components/responses/NotFound'
put:
summary: Update a TODO
parameters:
- name: todoId
in: path
required: true
schema:
type: integer
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TodoUpdate'
responses:
'200':
description: Successful response
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
'400':
$ref: '#/components/responses/BadRequest'
'404':
$ref: '#/components/responses/NotFound'
delete:
summary: Delete a TODO
parameters:
- name: todoId
in: path
required: true
schema:
type: integer
responses:
'204':
description: Successful response
'404':
$ref: '#/components/responses/NotFound'
components:
schemas:
Todo:
type: object
properties:
id:
type: integer
title:
type: string
completed:
type: boolean
required:
- id
- title
- completed
TodoCreate:
type: object
properties:
title:
type: string
completed:
type: boolean
default: false
required:
- title
TodoUpdate:
type: object
properties:
title:
type: string
completed:
type: boolean
minProperties: 1
responses:
BadRequest:
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Error:
type: object
properties:
message:
type: string
required:
- message
この OpenAPI 仕様は以下の機能を定義しています:
- TODOの一覧取得 (GET /todos)
- 新しいTODOの作成 (POST /todos)
- 特定のTODOの取得 (GET /todos/todoId)
- TODOの更新 (PUT /todos/todoId)
- TODOの削除 (DELETE /todos/todoId)
また、以下のスキーマも定義しています:
-
Todo
: 完全なTODOオブジェクト(id、title、completed を含む) -
TodoCreate
: 新しいTODOを作成する際に使用(title は必須、completed はオプション) -
TodoUpdate
: TODOを更新する際に使用(title と completed の少なくとも1つが必要) -
Error
: エラーレスポンス用のスキーマ
この OpenAPI 仕様は、先ほど作成した React コンポーネントと互換性があり、必要な全ての操作をカバーしています。API の実装者はこの仕様に基づいてバックエンドを構築し、フロントエンド開発者(React アプリケーションの開発者)はこの仕様を参照して API を利用することができます。
必要に応じて、認証やページネーションなどの機能を追加することで、より本格的な API 仕様にすることができます。
最後に
ここまでv0の概要と使用する際の流れを説明しました。
UIはもちろんのことAPIなんかも作れちゃうので、v0があればアプリ開発が捗りそうです。
ただ今のところUIコンポーネントはshadcn/uiやNext.jsに依存するので、あとはプロジェクトとの相性次第と思います。PoCなどはv0で良さそうですね。
まだPublicBetaということで、今後はMUIなど様々なUIコンポーネントに対応してもらえると嬉しいなと思いました。
以上です。ありがとうございました。