unnamed-ui
气泡/容器

Message

Message component for displaying AI and user messages in chat interfaces

你好,我想了解一下你们的产品
你好!很高兴为您介绍我们的产品。请问您对哪个方面比较感兴趣?
能详细介绍一下核心功能吗?
我们的产品主要有以下核心功能:
  • 智能对话系统
  • 多模态内容支持
  • 实时协作功能
"use client";

import { useState } from "react";
import {
  ComposedMessageList,
  MessageItem,
} from "@/components/wuhan/examples/message/message-composed-demo";

export function MessageDemo() {
  const [messages, setMessages] = useState<MessageItem[]>([
    {
      id: "1",
      role: "user",
      content: "你好,我想了解一下你们的产品",
      feedback: {
        onCopy: () => {},
      },
    },
    {
      id: "2",
      role: "assistant",
      content: "你好!很高兴为您介绍我们的产品。请问您对哪个方面比较感兴趣?",
      feedback: {
        onCopy: () => {},
      },
    },
    {
      id: "3",
      role: "user",
      content: "能详细介绍一下核心功能吗?",
      feedback: {
        onCopy: () => {},
      },
    },
    {
      id: "4",
      role: "assistant",
      content: (
        <div className="space-y-2">
          我们的产品主要有以下核心功能:
          <ul className="list-disc list-inside space-y-1 ml-2 mb-0">
            <li>智能对话系统</li>
            <li>多模态内容支持</li>
            <li>实时协作功能</li>
          </ul>
        </div>
      ),
      feedback: {
        onCopy: () => {},
      },
    },
  ]);

  const handleFeedbackChange = (
    id: string,
    feedback: Partial<MessageItem["feedback"]>,
  ) => {
    setMessages((prev) =>
      prev.map((msg) =>
        msg.id === id
          ? {
              ...msg,
              feedback: {
                ...msg.feedback,
                ...feedback,
              },
            }
          : msg,
      ),
    );
  };

  return (
    <ComposedMessageList
      messages={messages}
      onFeedbackChange={handleFeedbackChange}
      className="max-w-full"
    />
  );
}

Message 组件提供了用于显示 AI 消息和用户消息的完整样式组件,支持消息状态管理、反馈区域、附件展示等功能,适用于聊天界面、对话系统、AI 助手等场景。

概述

  • 两种消息类型:AIMessage(AI 消息)和 UserMessage(用户消息),样式自动区分
  • 状态管理:AI 消息支持 idle、generating、failed 三种状态
  • 内容渲染:根据状态智能渲染内容,生成中显示 loading,失败显示错误信息
  • 反馈区域:内置反馈插槽,方便添加点赞、复制等操作
  • 样式隔离:消息样式不受文档框架影响,保持一致性
  • 无障碍访问:支持 ARIA live regions,屏幕阅读器友好

快速开始

import { AIMessage, UserMessage } from "@/registry/wuhan/composed/message";

export function Example() {
  return (
    <div className="space-y-4">
      <UserMessage>
        请帮我解释一下 React Hooks 的工作原理
      </UserMessage>
      
      <AIMessage 
        status="idle"
        content="React Hooks 是 React 16.8 引入的新特性..."
      />
    </div>
  );
}

特性

  • AI 消息状态:idle(正常)、generating(生成中)、failed(失败)三种状态
  • 智能内容渲染:根据状态自动渲染 content 或 children
  • 用户消息:简洁的用户消息样式,带背景色区分
  • 反馈支持:通过 feedback 属性添加操作按钮
  • 附件支持:可在消息上方展示附件(图片、文件等)
  • 错误处理:失败状态自动显示错误信息

安装

pnpm dlx shadcn@latest add http://localhost:3000/r/wuhan/message.json

代码演示

基本

基础的消息展示,包含用户消息和 AI 消息。

这是一条用户消息
这是一条 AI 消息
"use client";

import {
  AIMessage,
  UserMessage,
} from "@/components/composed/message/message";

export function MessageDefault() {
  return (
    <div className="flex flex-col gap-4 items-center w-full">
      <UserMessage>这是一条用户消息</UserMessage>
      <AIMessage>这是一条 AI 消息</AIMessage>
    </div>
  );
}

状态的消息

展示 AI 消息的不同状态,包括生成中状态和生成失败状态。

正常状态

这是一条正常的 AI 消息,显示完整的内容。

生成中状态(使用原语组件)

正在思考中...

生成中状态(自定义)

原始内容(不会显示)正在生成回复,请稍候...

生成失败状态(默认)

生成失败,请稍后重试

生成失败状态(使用原语组件)

生成失败,请稍后重试

生成失败状态(带重试,使用原语组件)

生成失败,请稍后重试

交互式演示

这是一条正常的 AI 消息回复。
"use client";

import { useState } from "react";
import { AIMessage } from "@/components/composed/message/message";
import {
  LoadingDots,
  MessageGeneratingPrimitive,
  MessageFailedPrimitive,
} from "@/components/wuhan/blocks/message/message-01";
import { Button } from "@/components/ui/button";
import { AlertCircle, RefreshCw } from "lucide-react";

export function MessageWithStatus() {
  const [status, setStatus] = useState<"idle" | "generating" | "failed">(
    "idle",
  );

  const handleGenerate = () => {
    setStatus("generating");
    // 模拟生成过程
    setTimeout(() => {
      setStatus("idle");
    }, 2000);
  };

  const handleFail = () => {
    setStatus("failed");
  };

  const handleRetry = () => {
    setStatus("generating");
    setTimeout(() => {
      setStatus("idle");
    }, 2000);
  };

  return (
    <div className="flex flex-col gap-6 w-full h-full max-w-2xl">
      {/* 示例 1: 正常状态 */}
      <div className="space-y-2">
        <h3 className="text-sm font-medium text-[var(--Text-text-secondary)]">
          正常状态
        </h3>
        <div className="flex justify-start w-full">
          <AIMessage status="idle">
            这是一条正常的 AI 消息,显示完整的内容。
          </AIMessage>
        </div>
      </div>

      {/* 示例 2: 生成中状态 - 使用原语组件 */}
      <div className="space-y-2">
        <h3 className="text-sm font-medium text-[var(--Text-text-secondary)]">
          生成中状态(使用原语组件)
        </h3>
        <div className="flex justify-start w-full">
          <AIMessage
            status="generating"
            generatingContent={
              <MessageGeneratingPrimitive
                indicator={<LoadingDots />}
                text="正在思考中..."
              />
            }
          />
        </div>
      </div>

      {/* 示例 3: 生成中状态 - 自定义内容 */}
      <div className="space-y-2">
        <h3 className="text-sm font-medium text-[var(--Text-text-secondary)]">
          生成中状态(自定义)
        </h3>
        <div className="flex justify-start w-full">
          <AIMessage
            status="generating"
            generatingContent={
              <span className="text-[var(--Text-text-secondary)]">
                正在生成回复,请稍候...
              </span>
            }
          >
            原始内容(不会显示)
          </AIMessage>
        </div>
      </div>

      {/* 示例 4: 生成失败状态 - 使用默认错误消息 */}
      <div className="space-y-2">
        <h3 className="text-sm font-medium text-[var(--Text-text-secondary)]">
          生成失败状态(默认)
        </h3>
        <div className="flex justify-start w-full">
          <AIMessage status="failed" errorMessage="生成失败,请稍后重试">
            原始内容(不会显示)
          </AIMessage>
        </div>
      </div>

      {/* 示例 5: 生成失败状态 - 使用原语组件 */}
      <div className="space-y-2">
        <h3 className="text-sm font-medium text-[var(--Text-text-secondary)]">
          生成失败状态(使用原语组件)
        </h3>
        <div className="flex justify-start w-full">
          <AIMessage
            status="failed"
            errorContent={
              <MessageFailedPrimitive
                icon={
                  <AlertCircle className="size-4 text-[var(--Text-text-error)]" />
                }
                message="生成失败,请稍后重试"
              />
            }
          >
            原始内容(不会显示)
          </AIMessage>
        </div>
      </div>

      {/* 示例 6: 生成失败状态 - 带重试按钮(使用原语组件) */}
      <div className="space-y-2">
        <h3 className="text-sm font-medium text-[var(--Text-text-secondary)]">
          生成失败状态(带重试,使用原语组件)
        </h3>
        <div className="flex justify-start w-full">
          <AIMessage
            status="failed"
            errorContent={
              <MessageFailedPrimitive
                icon={
                  <AlertCircle className="size-4 text-[var(--Text-text-error)]" />
                }
                message="生成失败,请稍后重试"
                actions={
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={handleRetry}
                    className="w-fit"
                  >
                    <RefreshCw className="size-3 mr-1" />
                    重试
                  </Button>
                }
              />
            }
          >
            原始内容(不会显示)
          </AIMessage>
        </div>
      </div>

      {/* 示例 7: 交互式演示 */}
      <div className="space-y-4 p-4 border rounded-lg">
        <h3 className="text-sm font-medium">交互式演示</h3>
        <div className="flex justify-start w-full">
          <AIMessage
            status={status}
            generatingContent={
              <MessageGeneratingPrimitive
                indicator={<LoadingDots />}
                text="正在生成回复..."
              />
            }
            errorMessage="生成失败,请稍后重试"
            errorContent={
              <MessageFailedPrimitive
                icon={
                  <AlertCircle className="size-4 text-[var(--Text-text-error)]" />
                }
                message="生成失败"
                actions={
                  <Button
                    variant="outline"
                    size="sm"
                    onClick={handleRetry}
                    className="w-fit"
                  >
                    <RefreshCw className="size-3 mr-1" />
                    重试
                  </Button>
                }
              />
            }
          >
            {status === "idle" && "这是一条正常的 AI 消息回复。"}
          </AIMessage>
        </div>
        <div className="flex gap-2">
          <Button onClick={handleGenerate} disabled={status === "generating"}>
            模拟生成
          </Button>
          <Button
            onClick={handleFail}
            variant="destructive"
            disabled={status === "generating"}
          >
            模拟失败
          </Button>
        </div>
      </div>
    </div>
  );
}

消息反馈

带反馈区域的消息,可以在消息下方添加操作按钮。

请帮我解释一下 React Hooks
React Hooks 是 React 16.8 引入的新特性,允许你在函数组件中使用状态和其他 React 特性。常用的 Hooks 包括 useState、useEffect、useContext 等。
"use client";

import {
  AIMessage,
  UserMessage,
} from "@/components/composed/message/message";
import { Button } from "@/components/ui/button";
import {
  Tooltip,
  TooltipContent,
  TooltipTrigger,
} from "@/components/ui/tooltip";
import { ThumbsUp, ThumbsDown, Copy } from "lucide-react";
import { cn } from "@/lib/utils";

export function MessageWithFeedback() {
  return (
    <div className="flex flex-col gap-4 w-full">
      <div className="flex justify-end w-full">
        <UserMessage>请帮我解释一下 React Hooks</UserMessage>
      </div>
      <div className="flex justify-start w-full">
        <AIMessage
          feedback={
            <div className="flex items-center gap-[var(--Gap-gap-xs)]">
              <Tooltip>
                <TooltipTrigger asChild>
                  <Button
                    variant="ghost"
                    size="sm"
                    className="h-6 !px-1 cursor-pointer"
                  >
                    <ThumbsUp
                      className={cn(
                        "size-4",
                        "text-[var(--Text-text-secondary)]",
                      )}
                    />
                  </Button>
                </TooltipTrigger>
                <TooltipContent>
                  <p>点赞</p>
                </TooltipContent>
              </Tooltip>
              <Tooltip>
                <TooltipTrigger asChild>
                  <Button
                    variant="ghost"
                    size="sm"
                    className="h-6 !px-1 cursor-pointer"
                  >
                    <ThumbsDown
                      className={cn(
                        "size-4",
                        "text-[var(--Text-text-secondary)]",
                      )}
                    />
                  </Button>
                </TooltipTrigger>
                <TooltipContent>
                  <p>踩</p>
                </TooltipContent>
              </Tooltip>
              <Tooltip>
                <TooltipTrigger asChild>
                  <Button
                    variant="ghost"
                    size="sm"
                    className="h-6 !px-1 cursor-pointer"
                  >
                    <Copy
                      className={cn(
                        "size-4",
                        "text-[var(--Text-text-secondary)]",
                      )}
                    />
                  </Button>
                </TooltipTrigger>
                <TooltipContent>
                  <p>复制</p>
                </TooltipContent>
              </Tooltip>
            </div>
          }
        >
          React Hooks 是 React 16.8
          引入的新特性,允许你在函数组件中使用状态和其他 React 特性。常用的
          Hooks 包括 useState、useEffect、useContext 等。
        </AIMessage>
      </div>
    </div>
  );
}

显示附件

展示带附件的消息,附件显示在消息上方。

这是 AI 消息
文件名称.pdfPDF·14kb
这是用户消息,上方有附件
"use client";

import {
  UserMessage,
  AIMessage,
} from "@/components/composed/message/message";
import { AttachmentListComposed } from "@/components/composed/attachment-list/attachment-list";
import { FileText } from "lucide-react";

export function MessageWithAttachment() {
  return (
    <div className="flex flex-col gap-4 w-full">
      {/* AI 消息:带附件的消息 */}
      <div className="flex justify-start w-full">
        <div className="w-fit max-w-full flex flex-col gap-2">
          <AIMessage>这是 AI 消息</AIMessage>
        </div>
      </div>

      {/* 用户消息:带附件的消息 */}
      <div className="flex justify-end w-full">
        <div className="w-fit max-w-full flex flex-col gap-2">
          <AttachmentListComposed
            items={[
              {
                id: "file-1",
                name: "文件名称.pdf",
                fileType: "PDF",
                fileSize: "14kb",
                icon: <FileText className="size-4" />,
              },
            ]}
          />
          <UserMessage>这是用户消息,上方有附件</UserMessage>
        </div>
      </div>
    </div>
  );
}

流式消息渲染

模拟流式响应,逐字显示消息内容。

"use client";

import { AIMessage } from "@/components/composed/message/message";
import { useState, useEffect } from "react";

export function MessageStreaming() {
  const [content, setContent] = useState("");
  const [status, setStatus] = useState<"generating" | "idle">("generating");

  useEffect(() => {
    // 模拟流式响应
    const fullText =
      "这是一段流式生成的文本。React 是一个用于构建用户界面的 JavaScript 库,由 Meta(原 Facebook)开发和维护。它采用组件化的开发方式,使得代码更易于维护和复用。";
    let index = 0;

    const interval = setInterval(() => {
      if (index < fullText.length) {
        setContent(fullText.slice(0, index + 1));
        index++;
      } else {
        setStatus("idle");
        clearInterval(interval);
      }
    }, 50);

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

  return (
    <div className="h-[300px] w-full border rounded-lg overflow-hidden">
      <AIMessage
        status={status}
        generatingContent={status === "generating" ? content : undefined}
      >
        {status === "idle" ? content : null}
      </AIMessage>
    </div>
  );
}

带代码高亮

消息中包含代码块的示例。

这是一个 React 组件示例:

function Example() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}
"use client";

import { AIMessage } from "@/components/composed/message/message";

export function MessageWithCode() {
  return (
    <AIMessage status="idle">
      <div className="space-y-2">
        <p>这是一个 React 组件示例:</p>
        <pre className="bg-muted p-3 rounded-md overflow-x-auto">
          <code className="text-sm">{`function Example() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>
        Increment
      </button>
    </div>
  );
}`}</code>
        </pre>
      </div>
    </AIMessage>
  );
}

可重试的错误消息

失败状态的消息支持重试功能。

消息生成失败,请重试

"use client";

import { AIMessage } from "@/components/composed/message/message";
import { RefreshCw } from "lucide-react";
import { useState } from "react";

export function MessageRetryable() {
  const [status, setStatus] = useState<"failed" | "generating" | "idle">(
    "failed",
  );
  const [content, setContent] = useState("");

  const handleRetry = () => {
    setStatus("generating");
    // 模拟重试逻辑
    setTimeout(() => {
      setStatus("idle");
      setContent(
        "重试后成功生成的内容:React Hooks 是 React 16.8 引入的新特性,允许你在不编写 class 的情况下使用 state 和其他 React 特性。",
      );
    }, 2000);
  };

  return (
    <div className="h-[300px] w-full border rounded-lg overflow-hidden">
      <AIMessage
        status={status}
        errorContent={
          status === "failed" ? (
            <div className="space-y-3">
              <p className="text-destructive">消息生成失败,请重试</p>
              <button
                onClick={handleRetry}
                className="flex items-center gap-2 px-3 py-1.5 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors"
              >
                <RefreshCw className="w-4 h-4" />
                重试
              </button>
            </div>
          ) : undefined
        }
      >
        {status === "idle" ? content : null}
      </AIMessage>
    </div>
  );
}

API

AIMessage

AI 消息组件,支持状态管理和内容渲染。

Props

PropTypeDefaultDescription
status"idle" | "generating" | "failed"-AI 消息的生成状态(必填)
contentReactNode-消息内容,当 status 为 idle 时显示
childrenReactNode-替代 content 的内容,优先级更高
feedbackReactNode-反馈区域内容(如点赞、复制按钮)
classNamestring-额外的样式类名

Example

import { AIMessage } from "@/registry/wuhan/composed/message";
import { ThumbsUp, ThumbsDown, Copy } from "lucide-react";

function ChatMessage() {
  const [status, setStatus] = React.useState<"idle" | "generating" | "failed">("generating");
  const [content, setContent] = React.useState("");
  
  return (
    <div className="space-y-4">
      {/* 生成中状态 */}
      <AIMessage status="generating" />
      
      {/* 正常状态,显示内容 */}
      <AIMessage 
        status="idle"
        content="这是 AI 的回复内容,支持 Markdown 格式..."
        feedback={
          <div className="flex gap-2 mt-2">
            <button className="p-1 hover:bg-muted rounded">
              <ThumbsUp className="w-4 h-4" />
            </button>
            <button className="p-1 hover:bg-muted rounded">
              <ThumbsDown className="w-4 h-4" />
            </button>
            <button className="p-1 hover:bg-muted rounded">
              <Copy className="w-4 h-4" />
            </button>
          </div>
        }
      />
      
      {/* 失败状态 */}
      <AIMessage status="failed" />
      
      {/* 使用 children 而非 content */}
      <AIMessage status="idle">
        <div className="prose">
          <h3>自定义内容</h3>
          <p>可以使用任意 React 组件作为内容</p>
        </div>
      </AIMessage>
    </div>
  );
}

UserMessage

用户消息组件,简洁的样式设计。

Props

PropTypeDefaultDescription
childrenReactNode-消息内容(必填)
classNamestring-额外的样式类名

Example

import { UserMessage } from "@/registry/wuhan/composed/message";

function UserChatMessage() {
  return (
    <div className="space-y-4">
      <UserMessage>
        这是用户发送的消息
      </UserMessage>
      
      <UserMessage className="max-w-md">
        用户消息支持自定义样式
      </UserMessage>
    </div>
  );
}

AIMessageStatus

AI 消息状态类型定义。

type AIMessageStatus = 
  | "idle"        // 正常状态,显示完整内容
  | "generating"  // 生成中,显示 loading 动画
  | "failed";     // 生成失败,显示错误提示

使用场景

  • AI 聊天应用:ChatGPT、Claude 等 AI 对话界面
  • 客服系统:智能客服、在线咨询等场景
  • 文档问答:基于文档的问答系统
  • 代码助手:编程辅助、代码生成等工具
  • 教学助手:在线学习、答疑解惑等场景
  • 协作工具:团队沟通、任务分配等应用

最佳实践

  1. 状态管理:使用状态机管理消息的生成流程(generating → idle/failed)
  2. 错误处理:生成失败时提供重试按钮,提升用户体验
  3. 内容格式:AI 消息内容建议使用 Markdown 渲染,支持富文本
  4. 反馈收集:在 feedback 区域添加点赞、点踩,收集用户反馈
  5. 流式渲染:配合 SSE 或 WebSocket 实现流式内容更新
  6. 无障碍性:组件已内置 ARIA live regions,确保屏幕阅读器可访问

注意事项

  • children 优先级高于 content,两者同时存在时使用 children
  • status="generating" 时,contentchildren 会被忽略
  • status="failed" 时,显示默认错误信息,可通过 children 自定义
  • UserMessage 组件样式相对简单,主要用于区分用户和 AI 消息
  • 建议为长消息添加滚动或折叠功能,避免页面过长

原语组件

Message 基于以下原语组件构建:

  • MessageAIPrimitive - AI 消息容器原语
  • MessageUserPrimitive - 用户消息容器原语

原语组件提供了基础的样式和结构,可以在 registry/wuhan/blocks/message/message-01.tsx 中找到。

样式定制

组件使用 Tailwind CSS,可以通过以下方式定制:

// 自定义 AI 消息样式
<AIMessage 
  status="idle"
  content="内容"
  className="border-l-4 border-primary pl-4"
/>

// 自定义用户消息样式
<UserMessage className="bg-gradient-to-r from-blue-500 to-purple-500 text-white">
  自定义背景的用户消息
</UserMessage>