🦬

Vercel V0でAIにデスクトップアプリのGUIを作らせる【Tauri with Vercel V0】

2025/01/01に公開

はじめに

Vercel V0, 便利ですよね。Webフロントエンド開発の強いお供で、これ抜きでGUIを作るのは大変です。

Vercel V0で作ったコードはデスクトップアプリのGUI制作にも用いることができます。

今回はNext.js14.2とTauri2.0を用いて、Vercel V0に書かせたコードをデスクトップアプリのGUIにする方法を紹介します。

環境はMacで、npmの導入Rustの基本的な環境構築ができている想定で書かせていただきます。

Next.jsプロジェクトの設定まで

作業ディレクトリに移動してNext.jsプロジェクトを作成します。バージョンは14.2.3がTauriとの相性が保証されてていいらしいです。

npm create next-app@14.2

初期化する時に色々聞かれます。Vercel V0がshadcn/uiとTailwind CSSに依存するので下のように答えましょう。

> npx
> create-next-app
✔ **What is your project named?** … my-app #ここは任意
✔ **Would you like to use TypeScript?** … No / Yes #Yes
✔ **Would you like to use ESLint?** … No / Yes #Yes
✔ **Would you like to use Tailwind CSS?** … No / Yes #Yes
✔ **Would you like to use `src/` directory?** … No / Yes #Yes
✔ **Would you like to use App Router? (recommended)** … No / Yes #Yes
✔ **Would you like to customize the default import alias (@/*)?** … No / Yes #No

次にプロジェクトディレクトリに移動します。

cd my-app #さっき決めたproject名

プロジェクトディレクトリの中の構成はこんな感じになっています。

username@MacBook-Pro my-app % ls
README.md		package-lock.json	tailwind.config.ts
next-env.d.ts		package.json		tsconfig.json
next.config.mjs		postcss.config.mjs
node_modules		src

設定ファイルであるnext.config.mjsを次のように書き換えます

// 環境変数に基づいて本番環境かどうかを判定
// 'production'の場合はtrue、それ以外('development'など)の場合はfalse
const isProd = process.env.NODE_ENV === 'production';

// Tauriの開発サーバーのホストを設定
// 環境変数TAURI_DEV_HOSTが設定されていない場合は'localhost'を使用
const internalHost = process.env.TAURI_DEV_HOST || 'localhost';

// Next.jsの設定オブジェクト
const nextConfig = {
  // 静的サイト生成(SSG)モードを有効化
  // すべてのページを静的HTMLとしてビルド時に生成
  output: 'export',

  // Next.jsの画像最適化機能の設定
  // SSGモードで画像を使用するために最適化をオフにする
  images: {
    unoptimized: true,
  },

  // 静的アセット(画像、CSS、JSなど)のURL設定
  // 本番環境: undefined(デフォルトのパスを使用)
  // 開発環境: http://localhost:3000(または指定されたホスト)
  assetPrefix: isProd ? undefined : `http://${internalHost}:3000`,
};

// 設定をエクスポート
export default nextConfig;

Tauriの設定まで

先ほどと同じプロジェクトディレクトリでTauriの設定を進めていきます。
まずはRustのtauriクライアントを入れましょう

npm install -D @tauri-apps/cli@latest

次にtauriプロジェクトをセットアップします。

npx tauri init

このままではtauriのビルドコマンドなどが効かないので、package.jsonのscriptsに以下の行を追記します

  "scripts": {
    "tauri": "tauri", ##この行を手動で追加
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },

また、このままビルドするとBundle Identifierが一意にならずにエラーが発生するので、src-tauti/tauri.conf.jsonのidentifierを適当に編集します。

my-app/src-tauri/tauri.conf.json
{
  "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
  "productName": "my-app",
  "version": "0.1.0",
  "identifier": "com.chanmio.my-app", ##ここ
  "build": {
    "frontendDist": "../out",
    "devUrl": "http://localhost:3000",
    "beforeDevCommand": "npm run dev",
    "beforeBuildCommand": "npm run build"
  },
  "app": {
    "windows": [
      {
        "title": "my-app",
        "width": 800,
        "height": 600,
        "resizable": true,
        "fullscreen": false
      }
    ],
    "security": {
      "csp": null
    }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/128x128@2x.png",
      "icons/icon.icns",
      "icons/icon.ico"
    ]
  }
}

page.tsxの編集とshadcn/uiの導入

では、実際にGUIに表示したいページを書かせてみましょう。

試し切りに以下のコードをVercel V0に書かせてきました。

Vercel V0に書かせたコード
my-app/src/app/page.tsx
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Checkbox } from "@/components/ui/checkbox"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Slider } from "@/components/ui/slider"
import { Switch } from "@/components/ui/switch"
import { Textarea } from "@/components/ui/textarea"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Calendar } from "@/components/ui/calendar"
import { Separator } from "@/components/ui/separator"
import { Label } from "@/components/ui/label"
import { ChevronDown, Bell } from 'lucide-react'
import { Progress } from "@/components/ui/progress"

export default function Home() {

  return (
    <div className="container mx-auto p-4">
      <h1 className="text-3xl font-bold mb-6">shadcn/ui Component Showcase</h1>
      
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {/* Button and Dropdown Menu */}
        <Card>
          <CardHeader>
            <CardTitle>Buttons & Dropdown</CardTitle>
          </CardHeader>
          <CardContent className="space-y-4">
            <div className="space-x-2">
              <Button variant="default">Default</Button>
              <Button variant="destructive">Destructive</Button>
              <Button variant="outline">Outline</Button>
            </div>
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Button variant="outline">Open Menu <ChevronDown className="ml-2 h-4 w-4" /></Button>
              </DropdownMenuTrigger>
              <DropdownMenuContent>
                <DropdownMenuLabel>My Account</DropdownMenuLabel>
                <DropdownMenuSeparator />
                <DropdownMenuItem>Profile</DropdownMenuItem>
                <DropdownMenuItem>Billing</DropdownMenuItem>
                <DropdownMenuItem>Team</DropdownMenuItem>
                <DropdownMenuItem>Subscription</DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          </CardContent>
        </Card>

        {/* Accordion and Tabs */}
        <Card>
          <CardHeader>
            <CardTitle>Accordion & Tabs</CardTitle>
          </CardHeader>
          <CardContent>
            <Accordion type="single" collapsible className="mb-4">
              <AccordionItem value="item-1">
                <AccordionTrigger>Is it accessible?</AccordionTrigger>
                <AccordionContent>
                  Yes. It adheres to the WAI-ARIA design pattern.
                </AccordionContent>
              </AccordionItem>
            </Accordion>
            <Tabs defaultValue="account">
              <TabsList>
                <TabsTrigger value="account">Account</TabsTrigger>
                <TabsTrigger value="password">Password</TabsTrigger>
              </TabsList>
              <TabsContent value="account">Make changes to your account here.</TabsContent>
              <TabsContent value="password">Change your password here.</TabsContent>
            </Tabs>
          </CardContent>
        </Card>

        {/* Dialog */}
        <Card>
          <CardHeader>
            <CardTitle>Dialog</CardTitle>
          </CardHeader>
          <CardContent className="space-y-4">
            <Dialog>
              <DialogTrigger asChild>
                <Button variant="outline">Open Dialog</Button>
              </DialogTrigger>
              <DialogContent>
                <DialogHeader>
                  <DialogTitle>Are you sure absolutely sure?</DialogTitle>
                  <DialogDescription>
                    This action cannot be undone. This will permanently delete your account
                    and remove your data from our servers.
                  </DialogDescription>
                </DialogHeader>
              </DialogContent>
            </Dialog>
          </CardContent>
        </Card>

        {/* Input, Checkbox, and Radio */}
        <Card>
          <CardHeader>
            <CardTitle>Input, Checkbox & Radio</CardTitle>
          </CardHeader>
          <CardContent className="space-y-4">
            <Input type="email" placeholder="Email" />
            <div className="flex items-center space-x-2">
              <Checkbox id="terms" />
              <label
                htmlFor="terms"
                className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
              >
                Accept terms and conditions
              </label>
            </div>
            <RadioGroup defaultValue="comfortable">
              <div className="flex items-center space-x-2">
                <RadioGroupItem value="default" id="r1" />
                <Label htmlFor="r1">Default</Label>
              </div>
              <div className="flex items-center space-x-2">
                <RadioGroupItem value="comfortable" id="r2" />
                <Label htmlFor="r2">Comfortable</Label>
              </div>
              <div className="flex items-center space-x-2">
                <RadioGroupItem value="compact" id="r3" />
                <Label htmlFor="r3">Compact</Label>
              </div>
            </RadioGroup>
          </CardContent>
        </Card>

        {/* Select, Slider, and Switch */}
        <Card>
          <CardHeader>
            <CardTitle>Select, Slider & Switch</CardTitle>
          </CardHeader>
          <CardContent className="space-y-4">
            <Select>
              <SelectTrigger className="w-[180px]">
                <SelectValue placeholder="Theme" />
              </SelectTrigger>
              <SelectContent>
                <SelectItem value="light">Light</SelectItem>
                <SelectItem value="dark">Dark</SelectItem>
                <SelectItem value="system">System</SelectItem>
              </SelectContent>
            </Select>
            <Slider defaultValue={[33]} max={100} step={1} />
            <div className="flex items-center space-x-2">
              <Switch id="airplane-mode" />
              <Label htmlFor="airplane-mode">Airplane Mode</Label>
            </div>
          </CardContent>
        </Card>

        {/* Textarea, Progress, and Tooltip */}
        <Card>
          <CardHeader>
            <CardTitle>Textarea, Progress & Tooltip</CardTitle>
          </CardHeader>
          <CardContent className="space-y-4">
            <Textarea placeholder="Type your message here." />
            <Progress value={33} className="w-[60%]" />
            {/* Tooltip is missing from the original code and updates, so it's omitted here */}
          </CardContent>
        </Card>

        {/* Avatar, Badge, and Calendar */}
        <Card>
          <CardHeader>
            <CardTitle>Avatar, Badge & Calendar</CardTitle>
          </CardHeader>
          <CardContent className="space-y-4">
            <div className="flex items-center space-x-4">
              <Avatar>
                <AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
                <AvatarFallback>CN</AvatarFallback>
              </Avatar>
              <Badge variant="outline">
                <Bell className="mr-1 h-3 w-3" /> 5 Notifications
              </Badge>
            </div>
            <Calendar className="rounded-md border" />
          </CardContent>
        </Card>
      </div>
      
      <Separator className="my-6" />
      
      <footer className="text-center text-sm text-gray-500">
        © {new Date().getFullYear()} shadcn/ui Component Showcase. All rights reserved.
      </footer>
    </div>
  )
}



これをsrc/app/page.tsxにコピペします。

そして、Vercel V0に書かせたコードは大体shadcn/uiのコンポーネントを使っているので、Next.jsのプロジェクトディレクトリでshadcn/uiを導入して

npx shadcn@latest init

必要そうなコンポーネントをバコバコaddしていきます。

npx shadcn add accordion alert alert-dialog aspect-ratio avatar badge breadcrumb button calendar card carousel checkbox collapsible command context-menu dialog drawer dropdown-menu form hover-card input input-otp label menubar navigation-menu pagination popover progress radio-group resizable scroll-area select separator sheet skeleton slider sonner switch table tabs textarea toggle toggle-group tooltip

この辺は出てきたコードに応じて必要なものだけ入れましょう!

ビルド

ビルドは次のコマンドでできます

npm run tauri build

すると次のような画面がでて

Launchpadからアプリを起動すると無事Vercel V0で作成したGUIが見えました!

終わりに

記事を書くのは初めてなので、万が一何か地雷を踏んでたら教えてくれると幸いです...!よろしくお願いします。

Discussion