wui
组件

AI 快捷提问建议 AI Prompt Suggestions

用于新会话空状态页面、输入框上方或追问引导的预设提问卡片与标签组。

第三方依赖 · class-variance-authority第三方依赖 · lucide-react

基础用法

在新会话初始页面呈现精选推荐提问网格(Grid),帮助用户快速开启对话并直观了解 AI Agent 的能力边界:

Loading…

安装与引入

通过 CLI 自动添加组件,或手动复制源码至项目中:

pnpm dlx @wui-design/cli@latest add @wui/ai-prompt-suggestions
安装基础依赖与图标库
pnpm add lucide-react class-variance-authority clsx tailwind-merge
复制组件源码到 components/ui/ai-prompt-suggestions.tsx
components/ui/ai-prompt-suggestions.tsx
"use client"

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { ArrowUpRightIcon } from "lucide-react"

import { cn } from "@/lib/utils"

const aiPromptSuggestionsVariants = cva("w-full", {
  variants: {
    layout: {
      grid: "grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3",
      scroll:
        "flex snap-x snap-mandatory items-stretch gap-2 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden [&>*]:shrink-0 [&>*]:snap-start",
      chips: "flex flex-wrap items-center gap-1.5",
    },
  },
  defaultVariants: {
    layout: "grid",
  },
})

export interface AiPromptSuggestionsProps
  extends React.ComponentProps<"div">,
    VariantProps<typeof aiPromptSuggestionsVariants> {
  /** 快捷提示词组的排版布局方式(网格、横向滚动或胶囊标签)。 @default "grid" */
  layout?: "grid" | "scroll" | "chips"
}

/** 用于引导用户开启对话的预设提问卡片与标签组。子项挂载时依次错峰进入。 */
function AiPromptSuggestions({
  className,
  layout = "grid",
  children,
  ...props
}: AiPromptSuggestionsProps) {
  let order = 0

  return (
    <div
      data-slot="ai-prompt-suggestions"
      data-layout={layout}
      className={cn(aiPromptSuggestionsVariants({ layout }), className)}
      {...props}
    >
      {React.Children.map(children, (child) => {
        if (!React.isValidElement<{ style?: React.CSSProperties }>(child)) {
          return child
        }
        const delay = `${Math.min(order++, 10) * 40}ms`
        return React.cloneElement(child, {
          style: {
            "--tw-animation-delay": delay,
            ...child.props.style,
          } as React.CSSProperties,
        })
      })}
    </div>
  )
}

const aiPromptSuggestionItemVariants = cva(
  "group relative flex cursor-pointer select-none items-start text-left outline-none transition-[background-color,border-color,color,translate] duration-200 ease-out focus-visible:ring-[3px] focus-visible:ring-ring/35 disabled:pointer-events-none disabled:opacity-50 fill-mode-both animation-duration-300 motion-safe:animate-in motion-safe:fade-in-0 motion-safe:slide-in-from-bottom-1",
  {
    variants: {
      variant: {
        card: "flex-col gap-1 rounded-lg border bg-background p-3 hover:border-foreground/20 hover:bg-muted/40 motion-safe:hover:-translate-y-px motion-safe:active:translate-y-0",
        chip: "inline-flex items-center gap-1.5 rounded-full border bg-background px-3 py-1.5 text-xs text-muted-foreground hover:border-foreground/20 hover:bg-muted/60 hover:text-foreground",
        ghost: "flex-col gap-1 rounded-md p-2.5 hover:bg-muted/60",
      },
    },
    defaultVariants: {
      variant: "card",
    },
  }
)

export interface AiPromptSuggestionItemProps
  extends Omit<React.ComponentProps<"button">, "title">,
    VariantProps<typeof aiPromptSuggestionItemVariants> {
  /** Main prompt headline or question. */
  title: React.ReactNode
  /** Optional supporting description or detail. */
  description?: React.ReactNode
  /** Optional leading icon. */
  icon?: React.ReactNode
  /** Optional tag/badge indicator (e.g. "Popular", "Code"). */
  badge?: React.ReactNode
  /** Full prompt text to send when clicked. */
  promptText?: string
  /** Callback fired with the prompt text upon selection. */
  onSelectPrompt?: (prompt: string) => void
}

function AiPromptSuggestionItem({
  className,
  variant = "card",
  title,
  description,
  icon,
  badge,
  promptText,
  onSelectPrompt,
  onClick,
  ...props
}: AiPromptSuggestionItemProps) {
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    onClick?.(event)
    const text = promptText ?? (typeof title === "string" ? title : "")
    if (text) {
      onSelectPrompt?.(text)
    }
  }

  if (variant === "chip") {
    return (
      <button
        type="button"
        data-slot="ai-prompt-suggestion-item"
        data-variant={variant}
        className={cn(aiPromptSuggestionItemVariants({ variant }), className)}
        onClick={handleClick}
        {...props}
      >
        {icon ? (
          <span className="flex size-3.5 shrink-0 items-center justify-center text-muted-foreground transition-colors group-hover:text-foreground [&_svg]:size-3.5">
            {icon}
          </span>
        ) : null}
        <span className="truncate font-medium">{title}</span>
        {badge}
      </button>
    )
  }

  return (
    <button
      type="button"
      data-slot="ai-prompt-suggestion-item"
      data-variant={variant}
      className={cn(aiPromptSuggestionItemVariants({ variant }), className)}
      onClick={handleClick}
      {...props}
    >
      <div className="flex w-full items-center justify-between gap-2">
        <div className="flex min-w-0 items-center gap-2">
          {icon && (
            <span className="flex size-6 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground transition-colors group-hover:text-foreground [&_svg]:size-3.5">
              {icon}
            </span>
          )}
          <span className="truncate text-sm font-medium text-foreground">
            {title}
          </span>
        </div>
        <div className="flex shrink-0 items-center gap-1">
          {badge}
          <ArrowUpRightIcon className="size-3.5 -translate-x-0.5 translate-y-0.5 text-muted-foreground opacity-0 transition-[opacity,translate] duration-200 ease-out group-hover:translate-x-0 group-hover:translate-y-0 group-hover:opacity-100 group-focus-visible:opacity-100" />
        </div>
      </div>
      {description && (
        <p className="line-clamp-2 text-xs leading-5 text-muted-foreground">
          {description}
        </p>
      )}
    </button>
  )
}

export {
  AiPromptSuggestionItem,
  AiPromptSuggestions,
  aiPromptSuggestionItemVariants,
  aiPromptSuggestionsVariants,
}

属性 Props

AiPromptSuggestions Props

属性类型默认值说明
layout"grid" | "scroll" | "chips""grid"快捷建议组的整体排版模式。grid 为多列响应式网格,scroll 为横向滚动条,chips 为紧凑胶囊标签流。
classNamestring—应用于外层容器的额外 CSS 类名。

AiPromptSuggestionItem Props

属性类型默认值说明
titleReact.ReactNode—建议项的主要标题或问题文案。
descriptionReact.ReactNode—卡片模式下的辅助描述或示例细节说明。
variant"card" | "chip" | "ghost""card"单个条目的视觉外观变体。card 为独立卡片,chip 为紧凑圆角药丸,ghost 为扁平无边框风格。
iconReact.ReactNode—左侧展示的主题图标元素。
badgeReact.ReactNode—右上角或末尾的标签徽章,如「热门」、「代码」等 Badge 元素。
promptTextstring—点击后实际发送或填充的完整提示词字符串;若缺省则默认使用 title 文本。
onSelectPrompt(prompt: string) => void—用户点击该建议项时的回调函数,参数为解析后的完整提示词。

事件 Events

属性类型默认值说明
onSelectPrompt(prompt: string) => void—用户点击建议卡片或按键触发时调用,返回 promptText 或 title 文本,可直接注入输入框或触发发送。
onClick(event: React.MouseEvent<HTMLButtonElement>) => void—原生按钮点击事件透传。
onKeyDown(event: React.KeyboardEvent<HTMLButtonElement>) => void—原生键盘按键事件透传。

使用场景与设计规范

AiPromptSuggestions 用于冷启动引导与上下文追问:

  • 会话初始空状态 (Empty State):当用户首次打开聊天页面或新建对话时,空白界面容易造成“不知道该问什么”的困惑。提供 4~6 个代表性强、覆盖主要技能的 Prompt 卡片(layout="grid"),可以大幅提升新用户交互转化率。
  • 对话中途的上下文追问 (Follow-up Chips):在 AI 助手回答完毕后,可在输入框上方推荐 2~4 个相关的快捷追问标签(layout="chips"),引导用户继续深入探索。
  • 文案简明有力:卡片标题建议控制在 10 字以内,描述控制在 2 行以内,使用动宾结构(例如“生成代码架构”、“优化 SQL 查询”)。

场景示例

输入框上方的快捷追问胶囊

在输入框上方以 layout="chips" 展示紧凑药丸标签,点击后自动填入输入框:

Loading…

横向滑动的场景推荐列表

在空间受限的侧边栏或移动端界面中,使用 layout="scroll" 实现横向平滑滚动的提示词流:

Loading…

无障碍与交互 Accessibility

  • 原生按钮语义:每个 AiPromptSuggestionItem 均为原生 <button type="button">,天然具备可点击与可聚焦语义。
  • 键盘导航支持:
    • Tab:在建议项之间顺次切换焦点。
    • Enter / Space:选中并触发提示词填充。
  • 屏幕阅读器:内置图标采用装饰性隐藏或通过 title 提供完整语义表达。