🤖

claudeを試してみた

2024/06/24に公開

Claude の新機能 Artifacts

先日、anthropic の生成AIである Claude の新バージョン Claude 3.5 Sonnet がリリースされました。

https://www.anthropic.com/news/claude-3-5-sonnet

この Claude 3.5 Sonnet は性能自体も向上していて、GPT-4oにも優る性能のようです。

ただ、今回のアップデートでは Claude 3.5 Sonnet がリリースされただけでなく、「Artifacts」と呼ばれる新機能もリリースされています。

この Artifacts 機能では、出力されたコードを別窓で表示・管理をしてくれて、HTML等であればプレビューまでしてくれます。さらに、以前の出力をもとに修正ができ、修正を繰り返してコードをアップデートしていくことができます。

(プレビュー可能なものであれば)プレビューを見ながら、修正したい部分を指示して、開発を進めていくことができます。

これを使って今回は、以下の3つを試してみました。

  1. 丸投げでオセロをつくる
  2. 画像からつくる
  3. 簡易なTodoアプリの画面をつくる

Artifactsは有効化する必要があります。プロンプト入力部の下にフラスコマークがあります。

そのフラスコマークを押すと、Artifactsの設定があり、ここでArtifactsを有効化できます

1. 丸投げでオセロをつくる

さっそく、やっていきます。まずはオセロをつくって、と丸投げしてみます。

html,jsでcanvasを使ってオセロを作って

はい、できました、白と黒との石のオセロが。わずか数十秒です。PCだと右にArtifactsの枠がでてきて、そこにコードが生成されていきます。生成されたコードはプレビューできるので、実際にレンダリングされている様子も確認できます。もちろん、交互に石をおいて遊ぶことができます。

すこしカスタマイズしてみましょう。スコアを表示してみます。

canvasの下にスコアを表示して

再び、右のArtifactsにコードが生成されていきます。新たに生成されたコードのプレビューを見てみると、盤面下にスコアが表示されています。

もっとカスタマイズしてみましょう。

  • 石の色を変更
  • 盤面のサイズを変更
  • 勝敗がついたらどっちが勝ったのか表示させる
  • リセットボタン
石の色を赤と青に。盤面を6x6に。決着がついたら赤or青の勝ちって表示して、再戦ボタンを作って。

オセロをつくってカスタマイズしてみました。オセロくらい有名なゲームであれば一言いうだけでつくってくれます。Artifactsがあるのでプレビューも楽ですし、ステップを踏んだ変更もしやすいです。

最終的な生成コード
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>修正版オセロゲーム</title>
    <style>
        canvas {
            border: 1px solid black;
        }
        #score, #result {
            margin-top: 10px;
            font-size: 18px;
        }
        #resetButton {
            margin-top: 10px;
            font-size: 16px;
            padding: 5px 10px;
        }
    </style>
</head>
<body>
    <canvas id="gameBoard" width="300" height="300"></canvas>
    <div id="score">: 2 |: 2</div>
    <div id="result"></div>
    <button id="resetButton">再戦</button>
    <script>
        const canvas = document.getElementById('gameBoard');
        const ctx = canvas.getContext('2d');
        const scoreElement = document.getElementById('score');
        const resultElement = document.getElementById('result');
        const resetButton = document.getElementById('resetButton');
        const boardSize = 6;
        const cellSize = canvas.width / boardSize;
        let board = [];
        let currentPlayer = 1; // 1: 赤, -1: 青
        let gameOver = false;

        function initializeBoard() {
            board = Array(boardSize).fill().map(() => Array(boardSize).fill(0));
            board[2][2] = board[3][3] = -1;
            board[2][3] = board[3][2] = 1;
            currentPlayer = 1;
            gameOver = false;
            resultElement.textContent = '';
            updateScore();
        }

        function drawBoard() {
            ctx.fillStyle = 'green';
            ctx.fillRect(0, 0, canvas.width, canvas.height);

            for (let i = 1; i < boardSize; i++) {
                ctx.beginPath();
                ctx.moveTo(i * cellSize, 0);
                ctx.lineTo(i * cellSize, canvas.height);
                ctx.stroke();

                ctx.beginPath();
                ctx.moveTo(0, i * cellSize);
                ctx.lineTo(canvas.width, i * cellSize);
                ctx.stroke();
            }

            for (let row = 0; row < boardSize; row++) {
                for (let col = 0; col < boardSize; col++) {
                    if (board[row][col] !== 0) {
                        ctx.fillStyle = board[row][col] === 1 ? 'red' : 'blue';
                        ctx.beginPath();
                        ctx.arc(col * cellSize + cellSize / 2, row * cellSize + cellSize / 2, cellSize / 2 - 2, 0, Math.PI * 2);
                        ctx.fill();
                    }
                }
            }
        }

        function isValidMove(row, col) {
            if (board[row][col] !== 0) return false;

            const directions = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];

            for (let [dx, dy] of directions) {
                let x = row + dx;
                let y = col + dy;
                let flipped = false;

                while (x >= 0 && x < boardSize && y >= 0 && y < boardSize && board[x][y] === -currentPlayer) {
                    x += dx;
                    y += dy;
                    flipped = true;
                }

                if (flipped && x >= 0 && x < boardSize && y >= 0 && y < boardSize && board[x][y] === currentPlayer) {
                    return true;
                }
            }

            return false;
        }

        function makeMove(row, col) {
            if (gameOver || !isValidMove(row, col)) return false;

            board[row][col] = currentPlayer;
            const directions = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]];

            for (let [dx, dy] of directions) {
                let x = row + dx;
                let y = col + dy;
                let toFlip = [];

                while (x >= 0 && x < boardSize && y >= 0 && y < boardSize && board[x][y] === -currentPlayer) {
                    toFlip.push([x, y]);
                    x += dx;
                    y += dy;
                }

                if (x >= 0 && x < boardSize && y >= 0 && y < boardSize && board[x][y] === currentPlayer) {
                    for (let [fx, fy] of toFlip) {
                        board[fx][fy] = currentPlayer;
                    }
                }
            }

            currentPlayer = -currentPlayer;
            updateScore();
            checkGameOver();
            return true;
        }

        function updateScore() {
            let redScore = 0;
            let blueScore = 0;
            for (let row = 0; row < boardSize; row++) {
                for (let col = 0; col < boardSize; col++) {
                    if (board[row][col] === 1) redScore++;
                    if (board[row][col] === -1) blueScore++;
                }
            }
            scoreElement.textContent = `赤: ${redScore} | 青: ${blueScore}`;
        }

        function checkGameOver() {
            for (let row = 0; row < boardSize; row++) {
                for (let col = 0; col < boardSize; col++) {
                    if (isValidMove(row, col)) {
                        return; // ゲームが継続
                    }
                }
            }
            
            gameOver = true;
            let redScore = 0;
            let blueScore = 0;
            for (let row = 0; row < boardSize; row++) {
                for (let col = 0; col < boardSize; col++) {
                    if (board[row][col] === 1) redScore++;
                    if (board[row][col] === -1) blueScore++;
                }
            }
            
            if (redScore > blueScore) {
                resultElement.textContent = "赤の勝ち!";
            } else if (blueScore > redScore) {
                resultElement.textContent = "青の勝ち!";
            } else {
                resultElement.textContent = "引き分け!";
            }
        }

        canvas.addEventListener('click', (event) => {
            if (gameOver) return;
            const rect = canvas.getBoundingClientRect();
            const x = event.clientX - rect.left;
            const y = event.clientY - rect.top;
            const col = Math.floor(x / cellSize);
            const row = Math.floor(y / cellSize);

            if (makeMove(row, col)) {
                drawBoard();
            }
        });

        resetButton.addEventListener('click', () => {
            initializeBoard();
            drawBoard();
        });

        initializeBoard();
        drawBoard();
    </script>
</body>
</html>

2. 画像からつくる

次は、画像から丸投げで再現してもらいましょう。

shadcn の examples のダッシュボードの画像を再現してもらいます。もちろん、shadcnは使わないように付け足しておきます。

添付画像を可能な限り忠実に再現して。shadcnは使わないこと

添付画像は↓です。 (https://ui.shadcn.com/examples/dashboard)

こちらもすぐに生成されました。かなり再現度高く実装しされています。ライブラリとしては react, recharts, lucide-react を利用しています。これらはプレビューで描画できるようです。

かるくカスタマイズしてみましょう。棒グラフを折れ線グラフに変更します。変更した部分のコードだけが出力される場合があり、その場合にうまくプレビューできなくなってしまうので、既存コードも含め全体を出力するように指示します。

棒グラフを折れ線グラフに変更して。既存コードも含め全体を出力すること

他の部分は変えずに、グラフだけを折れ線グラフに変更してくれました。

最終的な生成コード
import React from 'react';
import { LineChart, Line, XAxis, YAxis, ResponsiveContainer } from 'recharts';
import { Download, UserCircle2 } from 'lucide-react';

const data = [
  { month: 'Jan', value: 2000 },
  { month: 'Feb', value: 3000 },
  { month: 'Mar', value: 1500 },
  { month: 'Apr', value: 1000 },
  { month: 'May', value: 4500 },
  { month: 'Jun', value: 4000 },
  { month: 'Jul', value: 2500 },
  { month: 'Aug', value: 5000 },
  { month: 'Sep', value: 3000 },
  { month: 'Oct', value: 2000 },
  { month: 'Nov', value: 3500 },
  { month: 'Dec', value: 4000 },
];

const recentSales = [
  { name: 'Olivia Martin', email: 'olivia.martin@email.com', amount: 1999.00 },
  { name: 'Jackson Lee', email: 'jackson.lee@email.com', amount: 39.00 },
  { name: 'Isabella Nguyen', email: 'isabella.nguyen@email.com', amount: 299.00 },
  { name: 'William Kim', email: 'will@email.com', amount: 99.00 },
  { name: 'Sofia Davis', email: 'sofia.davis@email.com', amount: 39.00 },
];

const Dashboard = () => {
  return (
    <div className="bg-gray-900 text-white p-6 min-h-screen">
      <header className="flex justify-between items-center mb-6">
        <div className="flex items-center space-x-4">
          <div className="w-3 h-3 bg-green-500 rounded-full"></div>
          <h1 className="text-xl font-semibold">Alicia Koch</h1>
        </div>
        <nav className="space-x-4">
          <a href="#" className="text-white">Overview</a>
          <a href="#" className="text-gray-400">Customers</a>
          <a href="#" className="text-gray-400">Products</a>
          <a href="#" className="text-gray-400">Settings</a>
        </nav>
        <div className="flex items-center space-x-4">
          <input type="text" placeholder="Search..." className="bg-gray-800 px-3 py-1 rounded"/>
          <UserCircle2 className="text-gray-400" />
        </div>
      </header>

      <main>
        <div className="flex justify-between items-center mb-6">
          <h2 className="text-3xl font-bold">Dashboard</h2>
          <div className="flex items-center space-x-4">
            <span className="bg-gray-800 px-3 py-1 rounded">Jan 20, 2023 - Feb 09, 2023</span>
            <button className="bg-white text-black px-3 py-1 rounded flex items-center">
              <Download className="mr-2" size={16} />
              Download
            </button>
          </div>
        </div>

        <nav className="mb-6">
          <a href="#" className="mr-4 text-white">Overview</a>
          <a href="#" className="mr-4 text-gray-400">Analytics</a>
          <a href="#" className="mr-4 text-gray-400">Reports</a>
          <a href="#" className="text-gray-400">Notifications</a>
        </nav>

        <div className="grid grid-cols-4 gap-4 mb-6">
          <div className="bg-gray-800 p-4 rounded">
            <h3 className="text-gray-400 mb-2">Total Revenue</h3>
            <p className="text-2xl font-bold">$45,231.89</p>
            <p className="text-green-500">+20.1% from last month</p>
          </div>
          <div className="bg-gray-800 p-4 rounded">
            <h3 className="text-gray-400 mb-2">Subscriptions</h3>
            <p className="text-2xl font-bold">+2350</p>
            <p className="text-green-500">+180.1% from last month</p>
          </div>
          <div className="bg-gray-800 p-4 rounded">
            <h3 className="text-gray-400 mb-2">Sales</h3>
            <p className="text-2xl font-bold">+12,234</p>
            <p className="text-green-500">+19% from last month</p>
          </div>
          <div className="bg-gray-800 p-4 rounded">
            <h3 className="text-gray-400 mb-2">Active Now</h3>
            <p className="text-2xl font-bold">+573</p>
            <p className="text-green-500">+201 since last hour</p>
          </div>
        </div>

        <div className="grid grid-cols-3 gap-4">
          <div className="col-span-2 bg-gray-800 p-4 rounded">
            <h3 className="text-xl font-semibold mb-4">Overview</h3>
            <ResponsiveContainer width="100%" height={300}>
              <LineChart data={data}>
                <XAxis dataKey="month" stroke="#6b7280" />
                <YAxis stroke="#6b7280" />
                <Line type="monotone" dataKey="value" stroke="#4ade80" strokeWidth={2} />
              </LineChart>
            </ResponsiveContainer>
          </div>
          <div className="bg-gray-800 p-4 rounded">
            <h3 className="text-xl font-semibold mb-4">Recent Sales</h3>
            <p className="text-gray-400 mb-4">You made 265 sales this month.</p>
            {recentSales.map((sale, index) => (
              <div key={index} className="flex justify-between items-center mb-4">
                <div className="flex items-center">
                  <UserCircle2 className="mr-2" size={32} />
                  <div>
                    <p>{sale.name}</p>
                    <p className="text-gray-400 text-sm">{sale.email}</p>
                  </div>
                </div>
                <p className="font-semibold">+${sale.amount.toFixed(2)}</p>
              </div>
            ))}
          </div>
        </div>
      </main>
    </div>
  );
};

export default Dashboard;

3. 簡易なTodoアプリをつくる

次に簡単なTodoアプリをつくります。今回はスマホからClaudeを操作してつくってもらいます。PCがなくてもスマホだけでこれだけのUIを作ることができるという例になればよいと思っています。

まずは、Todoの作成フォームからつくってもらいましょう。モーダルでフォームを実装してもらって、ボタンで開くようにします。今回は、使用するライブラリを指定しておきます。 (React、Tailwind CSS、lucide-react、recharts)

react, tailwind, lucide-react, rachartsのみを使ってよい。todoリストのtodo作成フォームを作って。タイトル、説明、担当者、期日を入力。ボタンで開くモーダルにして

さっそくTodo作成フォームがつくられました。しかし、shadcnが使われているので、使わないように指示します。

shadcnを使わないで

shadcnを使わずに実装してくれました。今は見た目だけの実装で「作成」ボタンを押しても何も発生しません。このあたりもうまく指示すればロジックの実装もやってもらえます。
今回は、ロジックの実装を進めるより、UIの実装を更にお願いします。ということで、次はTodoの一覧をつくってもらいましょう。各Todoをカードを並べる形で表示してもらいます。

ボタンの下に一覧表示して。それぞれのアイテムはカードで表示。完了未完了のアイコンを表示。完了ボタンも付けて。

生成が完了してもプレビューには、一覧表示されていません。コードを見てみると確かに一覧表示しているコードは生成されています。表示されていないのはTodoデータが空だからですね。サンプルデータを追加してみます。

この時点でのコード
import React, { useState } from 'react';
import { Plus, X, CheckCircle, Circle, Check } from 'lucide-react';

const TodoApp = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [assignee, setAssignee] = useState('');
  const [dueDate, setDueDate] = useState('');
  const [todos, setTodos] = useState([]);

  const handleSubmit = (e) => {
    e.preventDefault();
    const newTodo = {
      id: Date.now(),
      title,
      description,
      assignee,
      dueDate,
      completed: false
    };
    setTodos([...todos, newTodo]);
    setIsOpen(false);
    resetForm();
  };

  const resetForm = () => {
    setTitle('');
    setDescription('');
    setAssignee('');
    setDueDate('');
  };

  const toggleTodoCompletion = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  return (
    <div className="p-4 max-w-4xl mx-auto">
      <h1 className="text-2xl font-bold mb-4">TODOリスト</h1>
      
      <button
        onClick={() => setIsOpen(true)}
        className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors mb-6"
      >
        <Plus size={16} />
        新規TODO
      </button>

      {isOpen && (
        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4">
          <div className="bg-white rounded-lg p-6 w-full max-w-md">
            <div className="flex justify-between items-center mb-4">
              <h2 className="text-xl font-bold">新規TODOの作成</h2>
              <button onClick={() => setIsOpen(false)} className="text-gray-500 hover:text-gray-700">
                <X size={24} />
              </button>
            </div>
            <form onSubmit={handleSubmit} className="space-y-4">
              <div>
                <label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
                  タイトル
                </label>
                <input
                  id="title"
                  type="text"
                  value={title}
                  onChange={(e) => setTitle(e.target.value)}
                  required
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              </div>
              <div>
                <label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
                  説明
                </label>
                <textarea
                  id="description"
                  value={description}
                  onChange={(e) => setDescription(e.target.value)}
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                  rows={3}
                />
              </div>
              <div>
                <label htmlFor="assignee" className="block text-sm font-medium text-gray-700 mb-1">
                  担当者
                </label>
                <input
                  id="assignee"
                  type="text"
                  value={assignee}
                  onChange={(e) => setAssignee(e.target.value)}
                  required
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              </div>
              <div>
                <label htmlFor="dueDate" className="block text-sm font-medium text-gray-700 mb-1">
                  期日
                </label>
                <input
                  id="dueDate"
                  type="date"
                  value={dueDate}
                  onChange={(e) => setDueDate(e.target.value)}
                  required
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              </div>
              <div className="flex justify-end gap-2 mt-6">
                <button
                  type="button"
                  onClick={() => setIsOpen(false)}
                  className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
                >
                  キャンセル
                </button>
                <button
                  type="submit"
                  className="px-4 py-2 text-sm font-medium text-white bg-blue-500 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
                >
                  作成
                </button>
              </div>
            </form>
          </div>
        </div>
      )}

      <div className="space-y-4">
        {todos.map((todo) => (
          <div key={todo.id} className="bg-white shadow rounded-lg p-4 flex items-start">
            <div className="flex-shrink-0 mr-4">
              {todo.completed ? (
                <CheckCircle className="text-green-500" size={24} />
              ) : (
                <Circle className="text-gray-300" size={24} />
              )}
            </div>
            <div className="flex-grow">
              <h3 className="text-lg font-semibold mb-1">{todo.title}</h3>
              <p className="text-gray-600 mb-2">{todo.description}</p>
              <div className="text-sm text-gray-500">
                <p>担当者: {todo.assignee}</p>
                <p>期日: {todo.dueDate}</p>
              </div>
            </div>
            <button
              onClick={() => toggleTodoCompletion(todo.id)}
              className="ml-4 px-3 py-1 bg-blue-100 text-blue-600 rounded hover:bg-blue-200 transition-colors"
            >
              <Check size={16} />
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

export default TodoApp;
サンプルデータを入れて

表示されました。各アイテム右のチェックマークのボタンを押すことで、完了・未完了をトグルすることができます。
Todoアプリのメインの用途である、未完了のTodoのみを確認できるようにしたいです。

ボタンと一覧の間に、完了済のtodoを表示するか隠すかのラジオボックスを追加して。ロジックも実装して

すべてのTodoの表示と未完了のTodoのみの表示を切り替えれるようになりました。

次に完了日を管理できるように拡張します。また、完了日ごとの件数がわかるようなグラフを追加します。

各todoに完了日フィールドを追加して。一覧の一番上、ボタンの下に過去1週間の日毎の完了数を棒グラフで表示して

完了済のTodoはカードに完了日が表示されるようになりました。また、リスト上部にグラフが追加されています。
Todoのチェックマークのボタンを押して完了させると、グラフの今日の件数が増えていきます。

最後に、サンプルデータを追加してもらいます。

グラフが見栄えしないので、シンプルデータ数を増やして。合計30件くらいに

最終的な生成コード
import React, { useState, useEffect } from 'react';
import { Plus, X, CheckCircle, Circle, Check } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';

const generateSampleTodos = () => {
  const todos = [];
  const now = new Date();
  const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);

  const titles = [
    "プロジェクト企画書の作成", "クライアントミーティング", "バグ修正",
    "新機能の実装", "ドキュメント更新", "コードレビュー",
    "テスト作成", "デプロイメント準備", "パフォーマンス最適化",
    "セキュリティ監査", "ユーザーフィードバックの分析", "チームミーティング",
    "トレーニングセッション", "リファクタリング", "データベース最適化"
  ];

  for (let i = 0; i < 30; i++) {
    const randomDate = new Date(oneWeekAgo.getTime() + Math.random() * 7 * 24 * 60 * 60 * 1000);
    const completed = Math.random() > 0.3;
    todos.push({
      id: i + 1,
      title: titles[i % titles.length] + ` ${i + 1}`,
      description: `タスク ${i + 1} の詳細説明`,
      assignee: `担当者 ${i + 1}`,
      dueDate: new Date(now.getTime() + Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
      completed: completed,
      completedDate: completed ? randomDate.toISOString().split('T')[0] : null
    });
  }
  return todos;
};

const initialTodos = generateSampleTodos();

const TodoApp = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const [assignee, setAssignee] = useState('');
  const [dueDate, setDueDate] = useState('');
  const [todos, setTodos] = useState(initialTodos);
  const [showCompleted, setShowCompleted] = useState(true);
  const [completionData, setCompletionData] = useState([]);

  useEffect(() => {
    updateCompletionData();
  }, [todos]);

  const updateCompletionData = () => {
    const today = new Date();
    const oneWeekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
    
    const data = Array.from({ length: 7 }, (_, i) => {
      const date = new Date(oneWeekAgo.getTime() + (i + 1) * 24 * 60 * 60 * 1000);
      const count = todos.filter(todo => {
        const completedDate = new Date(todo.completedDate);
        return completedDate.toDateString() === date.toDateString();
      }).length;
      return {
        date: date.toISOString().split('T')[0],
        count,
      };
    });

    setCompletionData(data);
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const newTodo = {
      id: Date.now(),
      title,
      description,
      assignee,
      dueDate,
      completed: false,
      completedDate: null
    };
    setTodos(prevTodos => [...prevTodos, newTodo]);
    resetForm();
    setIsOpen(false);
  };

  const resetForm = () => {
    setTitle('');
    setDescription('');
    setAssignee('');
    setDueDate('');
  };

  const toggleTodoCompletion = (id) => {
    setTodos(prevTodos => prevTodos.map(todo =>
      todo.id === id
        ? { 
            ...todo, 
            completed: !todo.completed,
            completedDate: !todo.completed ? new Date().toISOString().split('T')[0] : null
          }
        : todo
    ));
  };

  const filteredTodos = showCompleted ? todos : todos.filter(todo => !todo.completed);

  return (
    <div className="p-4 max-w-4xl mx-auto">
      <h1 className="text-2xl font-bold mb-4">TODOリスト</h1>
      
      <button
        onClick={() => setIsOpen(true)}
        className="flex items-center gap-2 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition-colors mb-6"
      >
        <Plus size={16} />
        新規TODO
      </button>

      <div className="mb-6">
        <h2 className="text-xl font-semibold mb-2">過去1週間の完了数</h2>
        <ResponsiveContainer width="100%" height={300}>
          <BarChart data={completionData}>
            <XAxis dataKey="date" />
            <YAxis />
            <Tooltip />
            <Bar dataKey="count" fill="#4299e1" />
          </BarChart>
        </ResponsiveContainer>
      </div>

      <div className="mb-6">
        <label className="inline-flex items-center">
          <input
            type="radio"
            className="form-radio"
            name="showCompleted"
            value="all"
            checked={showCompleted}
            onChange={() => setShowCompleted(true)}
          />
          <span className="ml-2">全て表示</span>
        </label>
        <label className="inline-flex items-center ml-6">
          <input
            type="radio"
            className="form-radio"
            name="showCompleted"
            value="active"
            checked={!showCompleted}
            onChange={() => setShowCompleted(false)}
          />
          <span className="ml-2">未完了のみ表示</span>
        </label>
      </div>

      {isOpen && (
        <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
          <div className="bg-white rounded-lg p-6 w-full max-w-md">
            <div className="flex justify-between items-center mb-4">
              <h2 className="text-xl font-bold">新規TODOの作成</h2>
              <button onClick={() => setIsOpen(false)} className="text-gray-500 hover:text-gray-700">
                <X size={24} />
              </button>
            </div>
            <form onSubmit={handleSubmit} className="space-y-4">
              <div>
                <label htmlFor="title" className="block text-sm font-medium text-gray-700 mb-1">
                  タイトル
                </label>
                <input
                  id="title"
                  type="text"
                  value={title}
                  onChange={(e) => setTitle(e.target.value)}
                  required
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              </div>
              <div>
                <label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-1">
                  説明
                </label>
                <textarea
                  id="description"
                  value={description}
                  onChange={(e) => setDescription(e.target.value)}
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                  rows={3}
                />
              </div>
              <div>
                <label htmlFor="assignee" className="block text-sm font-medium text-gray-700 mb-1">
                  担当者
                </label>
                <input
                  id="assignee"
                  type="text"
                  value={assignee}
                  onChange={(e) => setAssignee(e.target.value)}
                  required
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              </div>
              <div>
                <label htmlFor="dueDate" className="block text-sm font-medium text-gray-700 mb-1">
                  期日
                </label>
                <input
                  id="dueDate"
                  type="date"
                  value={dueDate}
                  onChange={(e) => setDueDate(e.target.value)}
                  required
                  className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
                />
              </div>
              <div className="flex justify-end gap-2 mt-6">
                <button
                  type="button"
                  onClick={() => setIsOpen(false)}
                  className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-500"
                >
                  キャンセル
                </button>
                <button
                  type="submit"
                  className="px-4 py-2 text-sm font-medium text-white bg-blue-500 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500"
                >
                  作成
                </button>
              </div>
            </form>
          </div>
        </div>
      )}

      <div className="space-y-4">
        {filteredTodos.map((todo) => (
          <div key={todo.id} className="bg-white shadow rounded-lg p-4 flex items-start">
            <div className="flex-shrink-0 mr-4">
              {todo.completed ? (
                <CheckCircle className="text-green-500" size={24} />
              ) : (
                <Circle className="text-gray-300" size={24} />
              )}
            </div>
            <div className="flex-grow">
              <h3 className="text-lg font-semibold mb-1">{todo.title}</h3>
              <p className="text-gray-600 mb-2">{todo.description}</p>
              <div className="text-sm text-gray-500">
                <p>担当者: {todo.assignee}</p>
                <p>期日: {todo.dueDate}</p>
                {todo.completed && <p>完了日: {todo.completedDate}</p>}
              </div>
            </div>
            <button
              onClick={() => toggleTodoCompletion(todo.id)}
              className="ml-4 px-3 py-1 bg-blue-100 text-blue-600 rounded hover:bg-blue-200 transition-colors"
            >
              <Check size={16} />
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

export default TodoApp;

まとめ

Claude の新機能 Artifacts を使って、フロントのコードを生成してみました。

Claude 3.5 Sonnet も Artifacts も簡単に使えるのでぜひ色々試してみてください!

Discussion