Claudeは数分でガチWebアプリが作れるらしい

# 読み飛ばしてください

おはようございます、しなもんです。

最近やたらとTwitterのタイムラインにClaudeについての話が流れてきます。

https://x.com/alumican_net/status/1807742304410910879

**やべーな。**

んで、この`Claude Artifacts`という機能は無料でも使えるらしいので、
ちょいと使ってみることにしました。

# 始め方

https://claude.ai/new

アカウントを作って、ChatGPTライクな画面まで来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/831f4420-e321-d7cb-f478-381ed94e34f6.png)

そしたら**NEW Artifacts**と明らかに主張してきてるものがあるので、
`Try it out`から試してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/55d8ae31-aaf0-ce25-f335-781f81b020b6.png)

モーダルが出てくるので、Onにして戻ります。

# Artifacts機能を呼び出すにはコツがいる

Onにはしましたが、どうやらこれでもう使えるというわけではないらしい。

以下の条件下でのみ有効になるようです。

- ファイルの生成などに関わる内容
- ユーザーが修正したり保存する可能性が高い内容
- ファイル提示が必要な複雑な内容
- メールや資料など、チャットの外部で使用する可能性が高い内容

まずは適当に記事でも書いてもらってみましょう。
```:user
富士山に関する内容が記述されたマークダウンファイルを作成して
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/60c8eec6-8566-7e84-a46a-bb429be17e83.png)

このように、右からパネルが出現しました。
**これがArtifacts機能です。**

このファイルを見て、チャットしながら完成に近づけていくことができるのが特徴であるわけです。

```:user
おすすめの登山ルートを教えてくれる内容にしたい。
観光名所も含め、初心者向けに観光ガイドを書くようにしてほしい。
```
```:user
ちょっと長いかな。もっと短く、AルートとBルートの二つの選択肢を紹介するくらいで大丈夫。
行くときに事前に注意することも書いてほしい
```
```:user
内容はこれで大丈夫。あとは口調をもうちょっと軽く、身近なイメージに
```

<details><summary>最終的に生成されたマークダウンファイルを見る</summary>

```md
# 富士山登山：初心者さんいらっしゃい！

## 人気の2ルート、どっちにする？

### Aルート：吉田ルート
- **こんな人におすすめ**: 初めての富士山、のんびり派
- **スタート地点**: 富士スバルライン5合目（標高約2,300m）
- **登山時間**: 上り約6時間、下り約4時間
- **ここがポイント**: 
  - 山小屋がいっぱい！休憩し放題
  - 夜登って、ご来光を拝むならここだね

### Bルート：富士宮ルート
- **こんな人におすすめ**: 体力に自信アリ！景色重視派
- **スタート地点**: 富士宮口5合目（標高約2,400m）
- **登山時間**: 上り約5時間、下り約3時間
- **ここがポイント**:
  - 急な坂多めだけど、景色は最高！
  - 山小屋は少なめ、静かな山歩きを楽しみたい人向け

## 出発前に要チェック！

1. **高山病対策**: ゆっくり登って、こまめに休もう
2. **装備**: 防寒着、雨具、ヘッドライト、登山靴は必須だよ
3. **水分・食料**: 水とおやつは多めに持っていこう
4. **天気予報**: 出発前に最新チェック！天候急変もありえるからね
5. **体力づくり**: 事前トレーニングで、楽しい登山
6. **登山届**: 忘れずに出そう。安全第一！
7. **ごみ持ち帰り**: みんなで富士山キレイに保とう

さあ、準備はOK？ 富士山があなたを待ってるよ！楽しい登山にしようね！

```

</details>

なんかいい感じになりました。

最後の
**「さあ、準備はOK？ 富士山があなたを待ってるよ！楽しい登山にしようね！」**
がかっこよすぎます。

# 生成できるものはたくさんある

前章ではマークダウンファイルを生成してもらいましたが、他にも以下の種類のファイルが生成できるようです。

- Text
- Markdown
- HTML
- LaTeX
- SVG
- Code（pythonやjs、sqlなどのコード）
- Code（Mermaidを利用した図生成）
- Code（React）

画像生成やコード生成はいままでもありましたが、
**SVG生成やReact UI生成は凄いと思います。**

# 本題: Reactやってみようぜ

やってみます。

題材は...うーん...
Todoリストなんかは割と自力ですぐ作れるので
自力で作るのがだるいやつがいいな...

うーん、Three.jsで立体的に見れる仮想生命の生態系シミュレータとか...（過酷）
ダメ元でやってみましょう。

```:claude
はい、Three.jsを使って立体的に見える仮想生命の生態系シミュレータを作成することは可能です。
これは面白くて挑戦的なプロジェクトになりそうですね。
基本的な構造を示すReactコンポーネントを作成してみましょう。
```

**マジ？できちゃう？**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/5d815293-7f9a-07a6-a14d-4cbfd2236318.png)

プレビューでエラーが発生しています。
three.jsはダメだったかな...

```:user
three.jsはこの環境ではサポートされていないようです。
解決できる場合は解決し、できない場合はthree.jsを使わずに生態系シミュレーターを構築してください。
```

```:claude
申し訳ありません。Three.jsがサポートされていないことを理解しました。
その場合、Canvas APIを使用して2Dの生態系シミュレーターを作成しましょう。
これは単純化されたバージョンですが、同様の概念を示すことができます。
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/caef152d-7a28-8cc6-1079-4ad69158ca58.png)

**2秒で生成されたぞ。**
もうちょっといい感じにしてみましょう。

```:user
UIをもう少しSF風に、まるで研究しているようなイメージにしてください。
また、生物に種の概念を追加し、同じ種は色や形が同じになるようにしてください。
種の形は、正方形や六角形など、バリエーションを持たせて判別しやすいようにしてください。
背景はダークテーマっぽくしてみてください。
食物チェーンや繁殖の概念も追加してください。条件はあなたに任せます。
画面左上の方に統計情報を追加してください。
```

**結構むちゃぶりを伝えた気がします。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/8f2b59b6-a05e-8aa4-f93f-a6fbe5470f04.png)

**3秒でできちゃった。**

```:user
背景に粒子エフェクトなどを追加して微生物感を出してみてください。
このままでは食物連鎖が早すぎて全て絶滅してしまうので、バランスの調整をお願いします。
また、災害の要素を追加してみたいです。一定時間おきに一部エリアでエフェクトが表示され、その中にいる生物は消滅するようにしてください。この場合も、全ての生物が絶滅しないようにバランスの調整をお願いします。
また、進化メカニズムと詳細ビューの実装もお願いします。
```

さらに無茶を言ってみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/5bdfcb8f-c4ba-77c0-89fe-ba6415145b4c.png)

**すげぇぇぇぇーーー！！！！！！！！**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/d40f755e-55c0-a3fa-d1c3-ec579dbc199b.png)

それぞれの生物には種、エネルギー、年齢、世代、速度、捕食半径、繁殖率の概念があるようです。
マジかよ。

```:user
生物が絶滅するのが早すぎます。原因を考え、修正してください。
```
バランス調整もやってもらいます。

```:claude
エネルギー消費の調整：
エネルギー消費率を下げました（0.1から0.05に）。
新しい「エネルギー効率」特性を導入し、種ごとに異なる効率を設定しました。
これにより、生物がより長く生存できるようになります。

繁殖率の増加：
各種の繁殖率を若干増加させました（例：0.005から0.01に）。
繁殖のエネルギー閾値を150から120に下げました。
これにより、個体数の維持がしやすくなります。

捕食によるエネルギー獲得の増加：
捕食時のエネルギー獲得を20から30に増やしました。
これにより、捕食者がより効率的にエネルギーを獲得できます。

寿命の延長：
生物の最大年齢を200から300に増やしました。
これにより、生物がより長く生存し、繁殖の機会が増えます。

初期個体数の増加：
各種の初期個体数を20から30に増やしました。
これにより、シミュレーション開始時の絶滅リスクが減少します。

災害の影響を軽減：
災害の発生頻度を下げました（500から1000カウントに）。
災害の範囲と持続時間を減少させました。
災害による即死を避け、代わりにエネルギーを減少させるようにしました。
これにより、災害が生態系に与える影響が緩和されます。

進化メカニズム拡張：
エネルギー効率も進化の対象に加えました。
これにより、環境に適応した個体がより生存しやすくなります。

視覚効果の調整：
背景の不透明度を下げ、生物の軌跡が見やすくなるようにしました。
```

# 完成したものがこちら

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/0d8d931a-38b3-7a7f-adf4-d676c5aff89e.png)

上限が来てしまいましたので、6回ほど会話を繰り返して誕生したものを紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/69d7c51b-31db-10bd-0365-3458062819f8.png)

私がコードを一文字も書いていない生態系シミュレーターです。

寿命や、エネルギー、食料、季節、群れの形成、進化、繁殖、災害の概念があります。

<details><summary>コード</summary>

```react
import React, { useState, useEffect, useRef } from 'react';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';

const EcosystemSimulator = () => {
  const canvasRef = useRef(null);
  const [stats, setStats] = useState({ species1: 0, species2: 0, species3: 0 });
  const [selectedCreature, setSelectedCreature] = useState(null);
  const [season, setSeason] = useState('Spring');

  useEffect(() => {
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    const width = canvas.width;
    const height = canvas.height;

    // Species definitions
    const species = [
      { name: 'species1', color: '#FF5733', shape: 'circle', size: 8, speed: 1.2, eatRadius: 15, reproductionRate: 0.008, energyEfficiency: 0.95, flockingTendency: 0.2 },
      { name: 'species2', color: '#33FF57', shape: 'square', size: 10, speed: 0.9, eatRadius: 20, reproductionRate: 0.006, energyEfficiency: 0.97, flockingTendency: 0.4 },
      { name: 'species3', color: '#3357FF', shape: 'hexagon', size: 12, speed: 0.7, eatRadius: 25, reproductionRate: 0.004, energyEfficiency: 0.99, flockingTendency: 0.6 },
    ];

    // Seasons
    const seasons = ['Spring', 'Summer', 'Autumn', 'Winter'];
    let currentSeason = 0;
    let seasonCounter = 0;

    // Food source class
    class FoodSource {
      constructor() {
        this.x = Math.random() * width;
        this.y = Math.random() * height;
        this.energy = Math.random() * 50 + 50;
        this.size = Math.sqrt(this.energy);
      }

      draw() {
        ctx.fillStyle = '#8B4513';
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
        ctx.fill();
      }
    }

    // Create food sources
    let foodSources = Array(20).fill().map(() => new FoodSource());

    // Creature class
    class Creature {
      constructor(x, y, species) {
        this.x = x;
        this.y = y;
        this.species = { ...species };
        this.dx = (Math.random() - 0.5) * this.species.speed;
        this.dy = (Math.random() - 0.5) * this.species.speed;
        this.energy = 100;
        this.age = 0;
        this.generation = 1;
      }

      draw() {
        ctx.fillStyle = this.species.color;
        ctx.strokeStyle = '#FFFFFF';
        ctx.lineWidth = 1;

        switch (this.species.shape) {
          case 'circle':
            ctx.beginPath();
            ctx.arc(this.x, this.y, this.species.size, 0, Math.PI * 2);
            ctx.fill();
            ctx.stroke();
            break;
          case 'square':
            ctx.fillRect(this.x - this.species.size / 2, this.y - this.species.size / 2, this.species.size, this.species.size);
            ctx.strokeRect(this.x - this.species.size / 2, this.y - this.species.size / 2, this.species.size, this.species.size);
            break;
          case 'hexagon':
            ctx.beginPath();
            for (let i = 0; i < 6; i++) {
              const angle = (Math.PI / 3) * i;
              const x = this.x + this.species.size * Math.cos(angle);
              const y = this.y + this.species.size * Math.sin(angle);
              if (i === 0) ctx.moveTo(x, y);
              else ctx.lineTo(x, y);
            }
            ctx.closePath();
            ctx.fill();
            ctx.stroke();
            break;
        }
      }

      update(creatures, foodSources) {
        // Boundary check
        if (this.x + this.species.size > width || this.x - this.species.size < 0) this.dx = -this.dx;
        if (this.y + this.species.size > height || this.y - this.species.size < 0) this.dy = -this.dy;

        // Flocking behavior
        let flockX = 0, flockY = 0, flockCount = 0;
        creatures.forEach(other => {
          if (other !== this && other.species === this.species) {
            const distance = Math.hypot(this.x - other.x, this.y - other.y);
            if (distance < 50) {
              flockX += other.x;
              flockY += other.y;
              flockCount++;
            }
          }
        });

        if (flockCount > 0) {
          const centerX = flockX / flockCount;
          const centerY = flockY / flockCount;
          this.dx += (centerX - this.x) * this.species.flockingTendency * 0.01;
          this.dy += (centerY - this.y) * this.species.flockingTendency * 0.01;
        }

        this.x += this.dx;
        this.y += this.dy;

        // Energy consumption based on season
        let seasonalEnergyFactor = 1;
        switch (season) {
          case 'Summer': seasonalEnergyFactor = 1.2; break;
          case 'Winter': seasonalEnergyFactor = 0.8; break;
        }
        this.energy -= 0.02 * (2 - this.species.energyEfficiency) * seasonalEnergyFactor;
        this.age += 0.01;

        // Eating and reproduction
        creatures.forEach((other, index) => {
          if (other !== this) {
            const distance = Math.hypot(this.x - other.x, this.y - other.y);
            if (distance < this.species.eatRadius && this.species !== other.species) {
              this.energy += 30;
              creatures.splice(index, 1);
            } else if (distance < this.species.size * 2 && this.species === other.species && this.energy > 120 && Math.random() < this.species.reproductionRate) {
              this.energy -= 40;
              const offspring = new Creature(this.x, this.y, this.species);
              offspring.generation = this.generation + 1;
              this.evolve(offspring);
              creatures.push(offspring);
            }
          }
        });

        // Eating food sources
        foodSources.forEach((food, index) => {
          const distance = Math.hypot(this.x - food.x, this.y - food.y);
          if (distance < this.species.eatRadius + food.size) {
            this.energy += food.energy;
            foodSources.splice(index, 1);
          }
        });

        this.draw();
        return this.energy > 0 && this.age < 1000; // Extended lifespan
      }

      evolve(offspring) {
        const mutationRate = 0.1;
        const mutationAmount = 0.1;

        if (Math.random() < mutationRate) offspring.species.speed *= 1 + (Math.random() - 0.5) * mutationAmount;
        if (Math.random() < mutationRate) offspring.species.eatRadius *= 1 + (Math.random() - 0.5) * mutationAmount;
        if (Math.random() < mutationRate) offspring.species.reproductionRate *= 1 + (Math.random() - 0.5) * mutationAmount;
        if (Math.random() < mutationRate) {
          offspring.species.energyEfficiency *= 1 + (Math.random() - 0.5) * mutationAmount * 0.1;
          offspring.species.energyEfficiency = Math.min(Math.max(offspring.species.energyEfficiency, 0.9), 0.99);
        }
        if (Math.random() < mutationRate) {
          offspring.species.flockingTendency *= 1 + (Math.random() - 0.5) * mutationAmount;
          offspring.species.flockingTendency = Math.min(Math.max(offspring.species.flockingTendency, 0), 1);
        }
      }
    }

    // Create initial creatures
    let creatures = [];
    species.forEach(s => {
      for (let i = 0; i < 30; i++) {
        const x = Math.random() * width;
        const y = Math.random() * height;
        creatures.push(new Creature(x, y, s));
      }
    });

    // Animation loop
    function animate() {
      ctx.fillStyle = 'rgba(10, 10, 20, 0.1)';
      ctx.fillRect(0, 0, width, height);

      // Update and draw food sources
      foodSources.forEach(food => food.draw());

      // Update and draw creatures
      creatures = creatures.filter(creature => creature.update(creatures, foodSources));

      // Replenish food sources
      if (foodSources.length < 20 && Math.random() < 0.02) {
        foodSources.push(new FoodSource());
      }

      // Update season
      seasonCounter++;
      if (seasonCounter > 1000) {
        seasonCounter = 0;
        currentSeason = (currentSeason + 1) % 4;
        setSeason(seasons[currentSeason]);
      }

      // Update statistics
      const newStats = species.reduce((acc, s) => {
        acc[s.name] = creatures.filter(c => c.species.name === s.name).length;
        return acc;
      }, {});
      setStats(newStats);

      requestAnimationFrame(animate);
    }

    animate();

    // Add click event listener for creature selection
    canvas.addEventListener('click', (event) => {
      const rect = canvas.getBoundingClientRect();
      const x = event.clientX - rect.left;
      const y = event.clientY - rect.top;

      const clickedCreature = creatures.find(creature => 
        Math.hypot(x - creature.x, y - creature.y) < creature.species.size
      );

      setSelectedCreature(clickedCreature || null);
    });

    // Cleanup
    return () => {
      canvas.removeEventListener('click', () => {});
    };
  }, []);

  return (
    <div className="relative w-full h-screen bg-gray-900">
      <canvas ref={canvasRef} className="absolute top-0 left-0 w-full h-full" />
      <Card className="absolute top-4 left-4 bg-gray-800 text-white">
        <CardContent>
          <h3 className="text-lg font-bold mb-2">Ecosystem Statistics</h3>
          <p>Current Season: {season}</p>
          <ul>
            {Object.entries(stats).map(([species, count]) => (
              <li key={species}>{species}: {count}</li>
            ))}
          </ul>
        </CardContent>
      </Card>
      {selectedCreature && (
        <Card className="absolute top-4 right-4 bg-gray-800 text-white">
          <CardContent>
            <h3 className="text-lg font-bold mb-2">Creature Details</h3>
            <p>Species: {selectedCreature.species.name}</p>
            <p>Energy: {selectedCreature.energy.toFixed(2)}</p>
            <p>Age: {selectedCreature.age.toFixed(2)}</p>
            <p>Generation: {selectedCreature.generation}</p>
            <p>Speed: {selectedCreature.species.speed.toFixed(2)}</p>
            <p>Eat Radius: {selectedCreature.species.eatRadius.toFixed(2)}</p>
            <p>Reproduction Rate: {selectedCreature.species.reproductionRate.toFixed(4)}</p>
            <p>Energy Efficiency: {selectedCreature.species.energyEfficiency.toFixed(2)}</p>
            <p>Flocking Tendency: {selectedCreature.species.flockingTendency.toFixed(2)}</p>
            <Button className="mt-2" onClick={() => setSelectedCreature(null)}>Close</Button>
          </CardContent>
        </Card>
      )}
    </div>
  );
};

export default EcosystemSimulator;
```

</details>

プロトタイプにしては優秀すぎませんか。

# 最後に

Claude、すごいですね。
完成には程遠いと思いますが、ここまで動くくらいの生成能力は驚きました。

最後に一つだけ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1927831/84e396e7-e3f2-20ec-7463-db3e0b565736.png)

メモリ1GBくらい食べてるの、マジ？