5
3

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を生成AIで簡単に作る方法!

Last updated at Posted at 2024-09-23

はじめに

こんばんは。
本記事は 2024年 ANGEL Calendar 第22日として寄せるものです。
投稿予定日を疾うに過ぎ、臥待ちの月に見下ろされながら書いております。

本記事では、第15日の記事(【v0】UIを生成AIで簡単に作る方法!)が面白そうだったので、実際にやってみることにしました。
v0の概要などは元記事をご参照ください。

筆者はUIに関して無知識です。
実装の困難なもの、容易なもの、見当がつきませんが、果たして。

作りたいもの

今回は「アイゼンハワーマトリックス」の可視化ツールを作ります。

「アイゼンハワーマトリックス」とは、緊急度の高低、重要度の高低でタスクを4種に大別する考え方です。タスクの特性を踏まえることで、時間の有効活用を狙います。

matrix.png

詳細は他に委ねます。

マトリックスは等面積で描くこともできますが、時間配分を面積比で表現されたら更に実感が湧くはずです。
そのような既存ツールは見当たらなかったので自作してみます。1

実践

アカウント登録

メールアドレスの簡単な認証だけで済みました。

UI生成

画面左手でチャットが展開されます。
画面右手でUIとソースコードを確認できます。

overall.png


プロンプト1回目

期待:4つのストップウォッチを表示する。
結果:期待通り。

v1.png

依頼

ストップウォッチアプリを作ってください。
ストップウォッチは4つほしいです。
4つのストップウォッチは縦2列、横2行に並べてください。
それぞれのストップウォッチにスタート/一時停止ボタンを用意してください。
また、すべてのストップウォッチを同時にリセット出来るボタンも用意してください。
ストップウォッチは0時間0分0秒から23時間59分59秒まで対応できるようにしてください。
よろしくお願いします!

応答

はい、ご要望のストップウォッチアプリを作成いたします。4つのストップウォッチを2x2のグリッドレイアウトで配置し、各ストップウォッチにスタート/一時停止ボタンを設け、さらに全体をリセットするボタンも追加します。時間の表示は0時間0分0秒から23時間59分59秒までカバーします。

以下がReactコンポーネントの実装です:

multi-stopwatch.tsx
"use client"

import { useState, useEffect } from 'react'
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"

interface Stopwatch {
  time: number
  isRunning: boolean
}

export default function Component() {
  const [stopwatches, setStopwatches] = useState<Stopwatch[]>(
    Array(4).fill({ time: 0, isRunning: false })
  )

  useEffect(() => {
    const interval = setInterval(() => {
      setStopwatches(prevStopwatches =>
        prevStopwatches.map(sw =>
          sw.isRunning ? { ...sw, time: sw.time + 1 } : sw
        )
      )
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  const toggleStopwatch = (index: number) => {
    setStopwatches(prevStopwatches =>
      prevStopwatches.map((sw, i) =>
        i === index ? { ...sw, isRunning: !sw.isRunning } : sw
      )
    )
  }

  const resetAll = () => {
    setStopwatches(Array(4).fill({ time: 0, isRunning: false }))
  }

  const formatTime = (totalSeconds: number) => {
    const hours = Math.floor(totalSeconds / 3600)
    const minutes = Math.floor((totalSeconds % 3600) / 60)
    const seconds = totalSeconds % 60
    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
  }

  return (
    <div className="p-4 space-y-4">
      <div className="grid grid-cols-2 gap-4">
        {stopwatches.map((stopwatch, index) => (
          <Card key={index}>
            <CardContent className="p-4 flex flex-col items-center space-y-2">
              <div className="text-2xl font-bold">{formatTime(stopwatch.time)}</div>
              <Button onClick={() => toggleStopwatch(index)}>
                {stopwatch.isRunning ? '一時停止' : 'スタート'}
              </Button>
            </CardContent>
          </Card>
        ))}
      </div>
      <div className="flex justify-center">
        <Button onClick={resetAll} variant="outline">すべてリセット</Button>
      </div>
    </div>
  )
}

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

  1. 4つのストップウォッチを2x2のグリッドレイアウトで表示します。
  2. 各ストップウォッチには個別のスタート/一時停止ボタンがあります。
  3. 「すべてリセット」ボタンで、すべてのストップウォッチを同時に0にリセットできます。
  4. 時間は00:00:00から23:59:59まで表示可能です。
  5. 各ストップウォッチは独立して動作し、他のストップウォッチの状態に影響されません。

このコンポーネントは、Tailwind CSSを使用してスタイリングされており、レスポンシブデザインに対応しています。また、shadcn/uiのコンポーネントを使用して、一貫性のあるUIデザインを実現しています。

ご要望に応じて、さらなるカスタマイズや機能の追加も可能です。何か追加や変更したい点がございましたら、お知らせください。


プロンプト2回目

期待:経過時間を面積比に反映させる。
結果:いろいろ違う。
   これじゃ伝わらないだろうなと思いつつ投げたプロンプトとはいえ、
   マトリックスまで崩れたのは悲しい。前回の結果を褒めたのに。

v2.png

依頼

ありがとう、いい感じです。以下の追加をお願いします。

まず、それぞれのストップウォッチに名前を表示してください。
名前は以下の通りです。 左上:DO 右上:SCHEDULE 左下:DELEGATE 右下:ELIMINATE

次にそれぞれのストップウォッチを長方形のタイルにして、 経過時間に比例してタイルの面積が大きくなるようにしてほしいです。
この時、4つのタイル全体で長方形に見えるように、各タイルの辺の長さを調整してください。
もっとも経過時間の大きいストップウォッチのタイルが最も大きくなるはずです。
よろしくお願いします。

応答

ご要望ありがとうございます。ストップウォッチに名前を追加し、経過時間に応じてタイルのサイズを変更する機能を実装いたします。以下が更新されたコンポーネントです:

multi-stopwatch.tsx
 "use client"

import { useState, useEffect } from 'react'
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"

interface Stopwatch {
  name: string
  time: number
  isRunning: boolean
}

export default function Component() {
  const [stopwatches, setStopwatches] = useState<Stopwatch[]>([
    { name: "DO", time: 0, isRunning: false },
    { name: "SCHEDULE", time: 0, isRunning: false },
    { name: "DELEGATE", time: 0, isRunning: false },
    { name: "ELIMINATE", time: 0, isRunning: false }
  ])

  useEffect(() => {
    const interval = setInterval(() => {
      setStopwatches(prevStopwatches =>
        prevStopwatches.map(sw =>
          sw.isRunning ? { ...sw, time: sw.time + 1 } : sw
        )
      )
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  const toggleStopwatch = (index: number) => {
    setStopwatches(prevStopwatches =>
      prevStopwatches.map((sw, i) =>
        i === index ? { ...sw, isRunning: !sw.isRunning } : sw
      )
    )
  }

  const resetAll = () => {
    setStopwatches(stopwatches.map(sw => ({ ...sw, time: 0, isRunning: false })))
  }

  const formatTime = (totalSeconds: number) => {
    const hours = Math.floor(totalSeconds / 3600)
    const minutes = Math.floor((totalSeconds % 3600) / 60)
    const seconds = totalSeconds % 60
    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
  }

  const calculateSize = (time: number, maxTime: number) => {
    const minSize = 100
    const maxSize = 300
    return minSize + (maxSize - minSize) * (time / maxTime)
  }

  const maxTime = Math.max(...stopwatches.map(sw => sw.time))

  return (
    <div className="p-4 space-y-4">
      <div className="flex flex-wrap justify-center items-center gap-4" style={{ width: '100%', height: '80vh' }}>
        {stopwatches.map((stopwatch, index) => {
          const size = calculateSize(stopwatch.time, maxTime)
          return (
            <Card key={index} className="flex-grow" style={{ width: `${size}px`, height: `${size}px`, transition: 'all 0.5s ease-in-out' }}>
              <CardContent className="h-full p-4 flex flex-col justify-between items-center">
                <div className="text-xl font-bold">{stopwatch.name}</div>
                <div className="text-2xl font-bold">{formatTime(stopwatch.time)}</div>
                <Button onClick={() => toggleStopwatch(index)}>
                  {stopwatch.isRunning ? '一時停止' : 'スタート'}
                </Button>
              </CardContent>
            </Card>
          )
        })}
      </div>
      <div className="flex justify-center">
        <Button onClick={resetAll} variant="outline">すべてリセット</Button>
      </div>
    </div>
  )
}

この更新されたコンポーネントでは、以下の変更を行いました:

  1. 各ストップウォッチに名前を追加し、タイルの上部に表示しています。
  2. タイルのサイズを経過時間に比例して変更するように calculateSize 関数を実装しました。この関数は、最小サイズ100pxから最大サイズ300pxの間で、経過時間に応じてタイルのサイズを計算します。
  3. タイルのレイアウトをフレックスボックスに変更し、flex-wrap を使用して4つのタイルが全体として長方形になるように調整しました。
  4. 各タイルの widthheight を動的に設定し、最も経過時間の長いストップウォッチのタイルが最大になるようにしています。
  5. タイルのサイズ変更にアニメーションを追加し、よりスムーズな視覚効果を実現しています。

この実装により、各ストップウォッチの経過時間に応じてタイルのサイズが動的に変化し、4つのタイル全体で長方形を形成します。最も経過時間の長いストップウォッチのタイルが最も大きくなり、視覚的に時間の経過を表現しています。

さらなる調整や機能の追加が必要な場合は、お知らせください。


プロンプト3回目

期待:経過時間を面積比に反映させる。
結果:期待通り。
   スタートすると他のストップウォッチが消えてしまうのは想定内です。

v3.png

依頼 ありがとう、でも修正をお願いします。 まず、4つのストップウォッチは必ず2*2のグリッドレイアウトで表示してください。 左上がDO、右上がSCHEDULE、左下がDELEGATE、右下がELIMINATEです。

タイルのサイズについて、以下の条件を満たすように計算してください。
1つ目の条件です。「DOの経過時間とSCHEDULEの経過時間の合計」と「DELEGATEの経過時間とELIMINATEの経過時間の合計」の比が、「DOのタイル縦の長さ」と「DELEGATEのタイルの縦の長さ」の比に等しい。
2つ目の条件です。「DOのタイルの縦の長さ」と「SCHEDULEのタイルの縦の長さ」が等しい。
3つ目の条件です。「DELEGATEのタイルの縦の長さ」と「ELIMIATEのタイルの縦の長さ」が等しい。
4つ目の条件です。「DOの経過時間」と「SCHEDULEの経過時間」の比が、「DOのタイルの横の長さ」と「SCHEDULEのタイルの横の長さ」の比に等しい。
5つ目の条件です。「DELEGATEの経過時間」と「ELIMIATEの経過時間」の比が、「DELEGATEのタイルの横の長さ」と「ELIMIATEのタイルの横の長さ」の比に等しい。
6つ目の条件です。「DOのタイルの横の長さとSCHEDULEのタイルの横の長さの合計」が、「DELEGATEのタイルの横の長さとELIMINATEのタイルの横の長さの合計」に等しい。
7つ目の条件です。すべてのストップウォッチがリセットされているとき、4つのタイルの大きさがそれぞれ300pxになっている。

あなたなら絶対にできます。 よろしくお願いします。

応答 それでは、修正したコンポーネントを以下に示します:
multi-stopwatch.tsx
"use client"

import { useState, useEffect, useMemo } from 'react'
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"

interface Stopwatch {
  name: string
  time: number
  isRunning: boolean
}

interface TileSizes {
  [key: string]: { width: number; height: number }
}

const calculateTileSizes = (stopwatches: Stopwatch[]): TileSizes => {
  const [do_, schedule, delegate, eliminate] = stopwatches
  const totalTime = stopwatches.reduce((sum, sw) => sum + sw.time, 0)

  if (totalTime === 0) {
    return {
      DO: { width: 300, height: 300 },
      SCHEDULE: { width: 300, height: 300 },
      DELEGATE: { width: 300, height: 300 },
      ELIMINATE: { width: 300, height: 300 }
    }
  }

  const topRowTime = do_.time + schedule.time
  const bottomRowTime = delegate.time + eliminate.time
  const totalHeight = 600
  const totalWidth = 600

  const topHeight = (topRowTime / totalTime) * totalHeight
  const bottomHeight = totalHeight - topHeight

  const doWidth = (do_.time / topRowTime) * totalWidth
  const scheduleWidth = totalWidth - doWidth
  const delegateWidth = (delegate.time / bottomRowTime) * totalWidth
  const eliminateWidth = totalWidth - delegateWidth

  return {
    DO: { width: doWidth, height: topHeight },
    SCHEDULE: { width: scheduleWidth, height: topHeight },
    DELEGATE: { width: delegateWidth, height: bottomHeight },
    ELIMINATE: { width: eliminateWidth, height: bottomHeight }
  }
}

export default function Component() {
  const [stopwatches, setStopwatches] = useState<Stopwatch[]>([
    { name: "DO", time: 0, isRunning: false },
    { name: "SCHEDULE", time: 0, isRunning: false },
    { name: "DELEGATE", time: 0, isRunning: false },
    { name: "ELIMINATE", time: 0, isRunning: false }
  ])

  const tileSizes = useMemo(() => calculateTileSizes(stopwatches), [stopwatches])

  useEffect(() => {
    const interval = setInterval(() => {
      setStopwatches(prevStopwatches =>
        prevStopwatches.map(sw =>
          sw.isRunning ? { ...sw, time: sw.time + 1 } : sw
        )
      )
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  const toggleStopwatch = (index: number) => {
    setStopwatches(prevStopwatches =>
      prevStopwatches.map((sw, i) =>
        i === index ? { ...sw, isRunning: !sw.isRunning } : sw
      )
    )
  }

  const resetAll = () => {
    setStopwatches(stopwatches.map(sw => ({ ...sw, time: 0, isRunning: false })))
  }

  const formatTime = (totalSeconds: number) => {
    const hours = Math.floor(totalSeconds / 3600)
    const minutes = Math.floor((totalSeconds % 3600) / 60)
    const seconds = totalSeconds % 60
    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
  }

  return (
    <div className="p-4 space-y-4">
      <div className="grid grid-cols-2 gap-1" style={{ width: '600px', height: '600px' }}>
        {stopwatches.map((stopwatch, index) => (
          <Card 
            key={index} 
            className="flex flex-col justify-between overflow-hidden"
            style={{
              width: `${tileSizes[stopwatch.name].width}px`,
              height: `${tileSizes[stopwatch.name].height}px`,
              transition: 'all 0.5s ease-in-out'
            }}
          >
            <CardContent className="h-full p-4 flex flex-col justify-between items-center">
              <div className="text-xl font-bold">{stopwatch.name}</div>
              <div className="text-2xl font-bold">{formatTime(stopwatch.time)}</div>
              <Button onClick={() => toggleStopwatch(index)}>
                {stopwatch.isRunning ? '一時停止' : 'スタート'}
              </Button>
            </CardContent>
          </Card>
        ))}
      </div>
      <div className="flex justify-center mt-4">
        <Button onClick={resetAll} variant="outline">すべてリセット</Button>
      </div>
    </div>
  )
}

この修正されたコンポーネントでは、以下の変更を行いました:

  1. 2x2のグリッドレイアウトを維持し、指定された順序でストップウォッチを配置しています。

  2. calculateTileSizes 関数を実装し、7つの条件に基づいてタイルのサイズを計算しています。この関数は以下の条件を満たしています:

  3. 上下の行の高さ比が経過時間の合計比と等しくなるように計算

  4. 各行内でのタイルの幅比が経過時間比と等しくなるように計算

  5. すべてのストップウォッチがリセットされているとき、タイルのサイズが300x300pxになるように設定

  6. useMemo フックを使用して、stopwatchesの状態が変更されたときのみタイルサイズを再計算するようにしています。

  7. グリッドコンテナのサイズを600x600pxに固定し、各タイルのサイズを動的に調整しています。

  8. タイルのサイズ変更にアニメーションを追加し、よりスムーズな視覚効果を実現しています。

この実装により、指定されたすべての条件を満たしつつ、各ストップウォッチの経過時間に応じてタイルのサイズが動的に変化します。2x2のグリッドレイアウトを維持しながら、タイルのサイズが経過時間を視覚的に表現します。

さらなる調整や機能の追加が必要な場合は、お知らせください。


プロンプト4回目

期待:ストップウォッチの表示を固定する。
結果:背景色の配置、そうなるのか。

v4.png

依頼 ありがとう、素晴らしいです。 もうひとつだけ修正をお願いします。 4つのタイルを画面前後で二重にしてくだい。

画面手前のレイヤーにある4つのタイルにはストップウォッチを表示させます。
サイズはそれぞれ300pxで固定します。
4つのタイルはすべて無色透明にしてください。

画面奥のレイヤーにある4つのタイルにはストップウォッチは表示しません。
サイズは経過時間によって変化するように、前回の関数を使って計算してください。
4つのタイルの色は、左上が緑、右上が青、左下が黄色、右下がグレーです。

よろしくお願いします!

応答

それでは、修正したコンポーネントを以下に示します:

multi-stopwatch.tsx
"use client"

import { useState, useEffect, useMemo } from 'react'
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"

interface Stopwatch {
  name: string
  time: number
  isRunning: boolean
  color: string
}

interface TileSizes {
  [key: string]: { width: number; height: number }
}

const calculateTileSizes = (stopwatches: Stopwatch[]): TileSizes => {
  const [do_, schedule, delegate, eliminate] = stopwatches
  const totalTime = stopwatches.reduce((sum, sw) => sum + sw.time, 0)

  if (totalTime === 0) {
    return {
      DO: { width: 300, height: 300 },
      SCHEDULE: { width: 300, height: 300 },
      DELEGATE: { width: 300, height: 300 },
      ELIMINATE: { width: 300, height: 300 }
    }
  }

  const topRowTime = do_.time + schedule.time
  const bottomRowTime = delegate.time + eliminate.time
  const totalHeight = 600
  const totalWidth = 600

  const topHeight = (topRowTime / totalTime) * totalHeight
  const bottomHeight = totalHeight - topHeight

  const doWidth = (do_.time / topRowTime) * totalWidth
  const scheduleWidth = totalWidth - doWidth
  const delegateWidth = (delegate.time / bottomRowTime) * totalWidth
  const eliminateWidth = totalWidth - delegateWidth

  return {
    DO: { width: doWidth, height: topHeight },
    SCHEDULE: { width: scheduleWidth, height: topHeight },
    DELEGATE: { width: delegateWidth, height: bottomHeight },
    ELIMINATE: { width: eliminateWidth, height: bottomHeight }
  }
}

export default function Component() {
  const [stopwatches, setStopwatches] = useState<Stopwatch[]>([
    { name: "DO", time: 0, isRunning: false, color: "bg-green-500" },
    { name: "SCHEDULE", time: 0, isRunning: false, color: "bg-blue-500" },
    { name: "DELEGATE", time: 0, isRunning: false, color: "bg-yellow-500" },
    { name: "ELIMINATE", time: 0, isRunning: false, color: "bg-gray-500" }
  ])

  const tileSizes = useMemo(() => calculateTileSizes(stopwatches), [stopwatches])

  useEffect(() => {
    const interval = setInterval(() => {
      setStopwatches(prevStopwatches =>
        prevStopwatches.map(sw =>
          sw.isRunning ? { ...sw, time: sw.time + 1 } : sw
        )
      )
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  const toggleStopwatch = (index: number) => {
    setStopwatches(prevStopwatches =>
      prevStopwatches.map((sw, i) =>
        i === index ? { ...sw, isRunning: !sw.isRunning } : sw
      )
    )
  }

  const resetAll = () => {
    setStopwatches(stopwatches.map(sw => ({ ...sw, time: 0, isRunning: false })))
  }

  const formatTime = (totalSeconds: number) => {
    const hours = Math.floor(totalSeconds / 3600)
    const minutes = Math.floor((totalSeconds % 3600) / 60)
    const seconds = totalSeconds % 60
    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
  }

  return (
    <div className="p-4 space-y-4">
      <div className="relative" style={{ width: '600px', height: '600px' }}>
        {/* Background layer */}
        <div className="absolute inset-0 grid grid-cols-2 gap-1">
          {stopwatches.map((stopwatch, index) => (
            <div
              key={`bg-${index}`}
              className={`${stopwatch.color} transition-all duration-500 ease-in-out`}
              style={{
                width: `${tileSizes[stopwatch.name].width}px`,
                height: `${tileSizes[stopwatch.name].height}px`,
              }}
            />
          ))}
        </div>
        {/* Foreground layer */}
        <div className="absolute inset-0 grid grid-cols-2 gap-1">
          {stopwatches.map((stopwatch, index) => (
            <Card 
              key={`fg-${index}`}
              className="flex flex-col justify-between overflow-hidden bg-transparent"
              style={{
                width: '300px',
                height: '300px',
              }}
            >
              <CardContent className="h-full p-4 flex flex-col justify-between items-center">
                <div className="text-xl font-bold">{stopwatch.name}</div>
                <div className="text-2xl font-bold">{formatTime(stopwatch.time)}</div>
                <Button onClick={() => toggleStopwatch(index)}>
                  {stopwatch.isRunning ? '一時停止' : 'スタート'}
                </Button>
              </CardContent>
            </Card>
          ))}
        </div>
      </div>
      <div className="flex justify-center mt-4">
        <Button onClick={resetAll} variant="outline">すべてリセット</Button>
      </div>
    </div>
  )
}

この修正されたコンポーネントでは、以下の変更を行いました:

  1. 2つのレイヤーを作成しました:背景(奥)レイヤーと前景(手前)レイヤー。

  2. 背景レイヤー:

  3. 経過時間に応じてサイズが変化するタイルを配置

  4. 各タイルに指定された色(緑、青、黄、グレー)を適用

  5. calculateTileSizes 関数を使用してサイズを動的に計算

  6. 前景レイヤー:

  7. 固定サイズ(300px x 300px)のタイルを配置

  8. 背景を透明に設定(bg-transparent

  9. ストップウォッチの表示、操作ボタンを含む

  10. レイアウト:

  11. 両レイヤーを絶対位置指定で重ね合わせ

  12. グリッドレイアウトを使用して2x2の配置を維持

  13. アニメーション:

  14. 背景タイルのサイズ変更にトランジションを適用し、スムーズな変化を実現

  15. アクセシビリティ:

  16. カードコンポーネントを使用し、適切なコントラストと構造を維持

この実装により、画面手前に固定サイズの透明なタイルでストップウォッチを表示し、画面奥に経過時間に応じて変化する色付きのタイルを配置しています。ユーザーはストップウォッチを操作しながら、背景の色付きタイルの変化を視覚的に確認できます。

さらなる調整や機能の追加が必要な場合は、お知らせください。


プロンプト5回目 完成

期待:描画位置を修正する。
結果:期待通り!

v5.png

依頼

ありがとう!
それぞれのレイヤーで、4つのタイルの隙間が変化しないように、タイルの表示位置を修正してほしいです!

応答

それでは、修正したコンポーネントを以下に示します:

multi-stopwatch.tsx
"use client"

import { useState, useEffect, useMemo } from 'react'
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"

interface Stopwatch {
  name: string
  time: number
  isRunning: boolean
  color: string
}

interface TileSize {
  width: number
  height: number
  top: number
  left: number
}

interface TileSizes {
  [key: string]: TileSize
}

const GAP = 4 // Gap between tiles
const CONTAINER_SIZE = 600 // Total container size

const calculateTileSizes = (stopwatches: Stopwatch[]): TileSizes => {
  const [do_, schedule, delegate, eliminate] = stopwatches
  const totalTime = stopwatches.reduce((sum, sw) => sum + sw.time, 0)

  if (totalTime === 0) {
    const size = (CONTAINER_SIZE - GAP) / 2
    return {
      DO: { width: size, height: size, top: 0, left: 0 },
      SCHEDULE: { width: size, height: size, top: 0, left: size + GAP },
      DELEGATE: { width: size, height: size, top: size + GAP, left: 0 },
      ELIMINATE: { width: size, height: size, top: size + GAP, left: size + GAP }
    }
  }

  const topRowTime = do_.time + schedule.time
  const bottomRowTime = delegate.time + eliminate.time

  const topHeight = (topRowTime / totalTime) * (CONTAINER_SIZE - GAP)
  const bottomHeight = CONTAINER_SIZE - topHeight - GAP

  const doWidth = (do_.time / topRowTime) * (CONTAINER_SIZE - GAP)
  const scheduleWidth = CONTAINER_SIZE - doWidth - GAP
  const delegateWidth = (delegate.time / bottomRowTime) * (CONTAINER_SIZE - GAP)
  const eliminateWidth = CONTAINER_SIZE - delegateWidth - GAP

  return {
    DO: { width: doWidth, height: topHeight, top: 0, left: 0 },
    SCHEDULE: { width: scheduleWidth, height: topHeight, top: 0, left: doWidth + GAP },
    DELEGATE: { width: delegateWidth, height: bottomHeight, top: topHeight + GAP, left: 0 },
    ELIMINATE: { width: eliminateWidth, height: bottomHeight, top: topHeight + GAP, left: delegateWidth + GAP }
  }
}

export default function Component() {
  const [stopwatches, setStopwatches] = useState<Stopwatch[]>([
    { name: "DO", time: 0, isRunning: false, color: "bg-green-500" },
    { name: "SCHEDULE", time: 0, isRunning: false, color: "bg-blue-500" },
    { name: "DELEGATE", time: 0, isRunning: false, color: "bg-yellow-500" },
    { name: "ELIMINATE", time: 0, isRunning: false, color: "bg-gray-500" }
  ])

  const tileSizes = useMemo(() => calculateTileSizes(stopwatches), [stopwatches])

  useEffect(() => {
    const interval = setInterval(() => {
      setStopwatches(prevStopwatches =>
        prevStopwatches.map(sw =>
          sw.isRunning ? { ...sw, time: sw.time + 1 } : sw
        )
      )
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  const toggleStopwatch = (index: number) => {
    setStopwatches(prevStopwatches =>
      prevStopwatches.map((sw, i) =>
        i === index ? { ...sw, isRunning: !sw.isRunning } : sw
      )
    )
  }

  const resetAll = () => {
    setStopwatches(stopwatches.map(sw => ({ ...sw, time: 0, isRunning: false })))
  }

  const formatTime = (totalSeconds: number) => {
    const hours = Math.floor(totalSeconds / 3600)
    const minutes = Math.floor((totalSeconds % 3600) / 60)
    const seconds = totalSeconds % 60
    return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
  }

  return (
    <div className="p-4 space-y-4">
      <div className="relative" style={{ width: `${CONTAINER_SIZE}px`, height: `${CONTAINER_SIZE}px` }}>
        {/* Background layer */}
        <div className="absolute inset-0">
          {stopwatches.map((stopwatch, index) => (
            <div
              key={`bg-${index}`}
              className={`${stopwatch.color} absolute transition-all duration-500 ease-in-out`}
              style={{
                width: `${tileSizes[stopwatch.name].width}px`,
                height: `${tileSizes[stopwatch.name].height}px`,
                top: `${tileSizes[stopwatch.name].top}px`,
                left: `${tileSizes[stopwatch.name].left}px`,
              }}
            />
          ))}
        </div>
        {/* Foreground layer */}
        <div className="absolute inset-0">
          {stopwatches.map((stopwatch, index) => {
            const size = (CONTAINER_SIZE - GAP) / 2
            const top = index < 2 ? 0 : size + GAP
            const left = index % 2 === 0 ? 0 : size + GAP
            return (
              <Card 
                key={`fg-${index}`}
                className="absolute flex flex-col justify-between overflow-hidden bg-transparent"
                style={{
                  width: `${size}px`,
                  height: `${size}px`,
                  top: `${top}px`,
                  left: `${left}px`,
                }}
              >
                <CardContent className="h-full p-4 flex flex-col justify-between items-center">
                  <div className="text-xl font-bold">{stopwatch.name}</div>
                  <div className="text-2xl font-bold">{formatTime(stopwatch.time)}</div>
                  <Button onClick={() => toggleStopwatch(index)}>
                    {stopwatch.isRunning ? '一時停止' : 'スタート'}
                  </Button>
                </CardContent>
              </Card>
            )
          })}
        </div>
      </div>
      <div className="flex justify-center mt-4">
        <Button onClick={resetAll} variant="outline">すべてリセット</Button>
      </div>
    </div>
  )
}

この修正されたコンポーネントでは、以下の変更を行いました:

  1. GAPCONTAINER_SIZE の定数を定義し、タイルの隙間とコンテナのサイズを一定に保ちます。
  2. calculateTileSizes 関数を更新し、各タイルの位置(topleft)も計算するようにしました。これにより、背景レイヤーのタイルが正確に配置されます。
  3. 背景レイヤーのタイルを絶対位置指定に変更し、計算された位置とサイズに基づいて配置します。
  4. 前景レイヤーのタイルも絶対位置指定に変更し、固定サイズと位置で配置します。各タイルのサイズは (CONTAINER_SIZE - GAP) / 2 で計算し、位置はインデックスに基づいて決定します。
  5. コンテナのサイズを CONTAINER_SIZE に設定し、一定のサイズを維持します。

これらの変更により、背景レイヤーのタイルサイズが変化しても、前景レイヤーのタイルの位置と隙間が一定に保たれます。また、背景レイヤーのタイル間の隙間も一定に保たれます。

この実装では、ストップウォッチの機能を維持しながら、視覚的な一貫性と動的なレイアウト調整を実現しています。

さらなる調整や機能の追加が必要な場合は、お知らせください。


アカウント作成から完成まで20分程度でした。

所感

本当に作れた。

完成系のイメージだけで動くものができました。
自分でゼロから調べて作っていたら絶対にまだ完成していません。
すごいです。

生成時間は十分に早い。

送信後1分ほどで応答が終わり、UIサンプルが表示されました。

解説付きが嬉しい。

コード中のコメントではなく文章に要約されていたため、チャットに集中して進められました。

前のバージョンを追える。

ブラウザ上でUIとソースコードのバージョンを遡れます。
直感的でよいです。

versioned.png

ただしソースコードの差分は分かりにくいです。
前項とも重なりますが、対話形式での生成に特化している印象です。
コンセプトを絞って貫く美点はANGEL Dojoの活動でも大切なように感じます。

比例とは。。

2回目のプロンプトで「経過時間とタイル面積が比例するように」と書いてみましたが、線形には扱われませんでした。
ただ日本語でも非線形な場面で比例と呼ぶことはありますし、経過時間0のときに無表示だと困るのでv0の計らいは結果的には望ましいです。
敢えてそうしたのか適当に処理したのか語彙の問題なのか、気になるところです。

自然言語処理の発展は改めてすごい。

ChatGPTはこちらのぞんざいな文章に対しても
「おっけ!こういうことだよね?任せて!」
といった調子で返信を始めてくれるのがお約束です。
それに比べるとV0の返信は淡白な印象でした。
3回目の応答を受けて一瞬、(あれ、私何か怒らせるようなこと言ったかな・・・?)という戸惑いが走りました。
相手はAIなんですけれどもね。

まだエラー処理や表示速度など工夫の余地はありそうですが、今回はここまでです。
ご笑覧ありがとうございました。

  1. このマトリックスを活かした既存のタスク管理ツールとしては、例えば以下があります。時間配分を棒グラフで確認できます。
    Focus Matrix

5
3
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
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?