2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

v0によるUI作成の概要と流れ

Posted at

はじめに

手っ取り早くTODOアプリのUIを作る必要があったので、AIにコードを書いてもらうことにしました。調べてみるとVercel社のv0というAIツールがUIに特化しているという情報があったので試してみました。

v0の概要

v0とは

Next.jsの開発元であるVercel社が提供するWebデザインとコード生成に特化したAIツールです。以下の画面からプロンプトを実行することでコードを生成することができます。
image.png
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」を使っていきます。

当記事内でv0という単語は度々登場しますが、「Chat」の方を指します。

料金

以下の3プランが用意されています。今回はFreeプランを使っていきます。
(FreeプランでChatをどのくらい使えるのか、ドキュメントからは読み取れず、あまり分かってないです...)

image.png

公式ドキュメント

以下が公式のドキュメントになりますので、詳細はそちらをご覧ください。
https://v0.dev/docs

そのほか

クレジットの使用状況の確認方法

以下からクレジットの使用状況を確認できます。
Chatは不要と思いますが、Legacyの方を使用する場合に重要になります。

image.png
image.png
image.png

日本語化の設定方法

以下から言語設定を英語から日本語に変更できます。

image.png
image.png

実際に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>
  );
}

このコンポーネントは以下の機能を提供します:

  1. TODOの作成: 新しいTODOをテキスト入力し、「追加」ボタンをクリックして作成します。
  2. TODOの削除: 各TODOの「削除」ボタンをクリックして削除します。
  3. TODOの更新: 「編集」ボタンをクリックしてTODOを編集モードにし、変更後に「保存」ボタンをクリックして更新します。
  4. TODOの参照: コンポーネントがマウントされた時点で、すべてのTODOをAPIから取得して表示します。
  5. TODOの完了状態の切り替え: チェックボックスをクリックしてTODOの完了状態を切り替えます。

注意点:

  • API_BASE_URLを実際のAPIエンドポイントに置き換える必要があります。
  • エラーハンドリングは基本的なものしか実装していないので、実際の使用時にはより堅牢なエラーハンドリングを追加することをお勧めします。
  • このコンポーネントは基本的な機能を提供していますが、実際のアプリケーションではさらにスタイリングやエラー表示、ローディング状態の表示などを追加することで、ユーザー体験を向上させることができます。

このコードをベースに、必要に応じて機能を追加したり、スタイリングをカスタマイズしたりすることができます。

ローカルにNext.js環境を作成

次にローカルで画面を立ち上げて、TODOアプリを確認したいので環境を作ります。

v0では、CLIから生成したコードをベースとしたNext.jsのプロジェクトを立ち上げる機能を提供しています。

画面右上から「Add to Codebase」を開きます。

image.png

CLIコマンドが表示されるのでコピーします。

image.png

CLIコマンドを実行して、プロジェクトを作成します。

npx shadcn@latest add "https://v0.dev/chat/b/b_vowbOz3fa72?token=abcde12345"

結果の確認

Next.jsでローカル実行すると、先ほどのTODOアプリが表示されます。

以下は、左の画像がTODOアプリのTODOが空の状態で、右の画像が試しにTODOを追加して表示した状態となります。

初期表示 TODOを表示
image.png image.png

これだけで簡単にUIを作れちゃいました。

番外編:Claude 3.5 SonetでTODOアプリを生成

ここでふと他の生成AIでも別によくね?と思いまして、Claude 3.5 Sonetで同じプロンプトを試しました。結果としては、やはりv0の方がリッチな画面を作ってくれます。

実行結果

生成AI(Claude 3.5 Sonet)に同じプロンプトを流すと、以下のようになります。

初期表示 TODOを表示
image.png image.png

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 仕様は以下の機能を定義しています:

  1. TODOの一覧取得 (GET /todos)
  2. 新しいTODOの作成 (POST /todos)
  3. 特定のTODOの取得 (GET /todos/todoId)
  4. TODOの更新 (PUT /todos/todoId)
  5. 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コンポーネントに対応してもらえると嬉しいなと思いました。

以上です。ありがとうございました。

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?