Web 预览

一个可组合组件,用于预览生成的 UI 结果,支持实时示例和代码显示。

WebPreview 组件提供了一种灵活的方式来展示生成的 UI 组件的结果及其源代码。它专为文档和演示目的而设计,允许用户与实时示例交互并查看底层实现。

Install using CLI

AI Elements Vue
shadcn-vue CLI
npx ai-elements-vue@latest add web-preview

Install Manually

WebPreview.vue
WebPreviewBody.vue
WebPreviewConsole.vue
WebPreviewNavigation.vue
WebPreviewNavigationButton.vue
WebPreviewUrl.vue
context.ts
index.ts
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@repo/shadcn-vue/lib/utils'
import { computed, ref } from 'vue'
import {
  provideWebPreviewContext,
} from './context'

interface Props extends /* @vue-ignore */ HTMLAttributes {
  class?: HTMLAttributes['class']
  defaultUrl?: string
}

const props = withDefaults(defineProps<Props>(), {
  defaultUrl: '',
})

const emit = defineEmits<{
  (e: 'update:url', value: string): void
  (e: 'urlChange', value: string): void
  (e: 'update:consoleOpen', value: boolean): void
  (e: 'consoleOpenChange', value: boolean): void
}>()

const url = ref(props.defaultUrl)
const consoleOpen = ref(false)

function setUrl(value: string) {
  url.value = value
  emit('update:url', value)
  emit('urlChange', value)
}

function setConsoleOpen(value: boolean) {
  consoleOpen.value = value
  emit('update:consoleOpen', value)
  emit('consoleOpenChange', value)
}

provideWebPreviewContext({
  url,
  setUrl,
  consoleOpen,
  setConsoleOpen,
})

const vBind = computed(() => {
  const { class: _, ...rest } = props
  return {
    class: cn('flex size-full flex-col rounded-lg border bg-card', props.class),
    ...rest,
  }
})
</script>

<template>
  <div v-bind="vBind">
    <slot />
  </div>
</template>

与 AI SDK 一起使用

使用 v0 Platform API 构建一个简单的 v0 克隆。

安装 v0-sdk 包:

npm
pnpm
bun
yarn
npm i v0-sdk

将以下组件添加到你的前端:

app.vue
<script setup lang="ts">
import { Loader } from '@/components/ai-elements/loader'
import {
  Input,
  PromptInputSubmit,
  PromptInputTextarea,
} from '@/components/ai-elements/prompt-input'
import {
  WebPreview,
  WebPreviewBody,
  WebPreviewNavigation,
  WebPreviewUrl,
} from '@/components/ai-elements/web-preview'

const previewUrl = ref('')
const prompt = ref('')
const isGenerating = ref(false)

async function handleSubmit(e: Event) {
  e.preventDefault()
  if (!prompt.value.trim())
    return
  prompt.value = ''

  isGenerating.value = true
  try {
    const response = await fetch('/api/v0', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ prompt: prompt.value }),
    })

    const data = await response.json()
    previewUrl.value = data.demo || '/'
    console.log('Generation finished:', data)
  }
  catch (error) {
    console.error('Generation failed:', error)
  }
  finally {
    isGenerating.value = false
  }
}
</script>

<template>
  <div class="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
    <div class="flex flex-col h-full">
      <div class="flex-1 mb-4">
        <div v-if="isGenerating" class="flex flex-col items-center justify-center h-full">
          <Loader />
          <p v-if="isGenerating" class="mt-4 text-muted-foreground">
            Generating app, this may take a few seconds...
          </p>
        </div>
        <WebPreview v-else-if="previewUrl" :default-url="previewUrl">
          <WebPreviewNavigation>
            <WebPreviewUrl />
          </WebPreviewNavigation>
          <WebPreviewBody :src="previewUrl" />
        </WebPreview>
        <div v-else class="flex items-center justify-center h-full text-muted-foreground">
          Your generated app will appear here
        </div>
      </div>

      <Input
        class="w-full max-w-2xl mx-auto relative"
        @submit="handleSubmit"
      >
        <PromptInputTextarea
          :value="prompt"
          placeholder="Describe the app you want to build..."
          class="pr-12 min-h-[60px]"
          @change="(e: any) => (prompt = e?.target?.value ?? '')"
        >
          <PromptInputSubmit
            :status="isGenerating ? 'streaming' : 'ready'"
            :disabled="!prompt.trim()"
            class="absolute bottom-1 right-1"
          />
        </PromptInputTextarea>
      </Input>
    </div>
  </div>
</template>

将以下路由添加到你的后端:

server/api/v0.post.ts
import type { ChatsCreateResponse } from 'v0-sdk'
import { defineEventHandler, readBody } from 'h3'
import { v0 } from 'v0-sdk'

export default defineEventHandler(async (event) => {
  const { prompt }: { prompt: string } = await readBody(event)
  const result = await v0.chats.create({
    system: 'You are an expert coder',
    message: prompt,
    modelConfiguration: {
      modelId: 'v0-1.5-sm',
      imageGenerations: false,
      thinking: false,
    },
  }) as ChatsCreateResponse

  return {
    demo: result.demo,
    webUrl: result.webUrl,
  }
})

特性

  • UI 组件的实时预览
  • 具有专用子组件的可组合架构
  • 响应式设计模式(桌面、平板、移动)
  • 导航控件,具有后退/前进功能
  • URL 输入和示例选择器
  • 全屏模式支持
  • 带时间戳的控制台日志记录
  • 基于上下文的状态管理
  • 与设计系统一致的样式
  • 易于集成到文档页面

Props

<WebPreview />

defaultUrlstring
''
在预览中加载的初始 URL。
@urlChange(url: string) => void
URL 更改时触发的回调。
...propsHTMLAttributes
任何其他 props 都会传播到根 div。

<WebPreviewNavigation />

...propsHTMLAttributes
任何其他 props 都会传播到导航容器。

<WebPreviewNavigationButton />

tooltipstring
悬停时显示的工具提示文本。
...propstypeof Button
任何其他 props 都会传播到底层 shadcn-vue/ui Button 组件。

<WebPreviewUrl />

...propstypeof Input
任何其他 props 都会传播到底层 shadcn-vue/ui Input 组件。

<WebPreviewBody />

loadingSlot
在预览上显示的可选加载指示器。
...propsIframeHTMLAttributes
任何其他 props 都会传播到底层 iframe

<WebPreviewConsole />

logsArray<LogItem>
要在控制台面板中显示的控制台日志条目。
LogItem
type LogItem = { level: "log" | "warn" | "error"; message: string; timestamp: Date }
Example
[
  {
    "level": "log",
    "message": "Page loaded successfully",
    "timestamp": "2025-01-01T00:00:00.000Z"
  }
]
...propsHTMLAttributes
任何其他 props 都会传播到根 div。