unnamed-ui
气泡/容器

Message List

Chat message list component with auto-scroll, multiple message types, and status support

用户10:30
请分析一下这个问题
AI 助手10:31
已从多维度分析问题,并生成详细方案与建议。
"use client";

import {
  MessageList,
  type MessageItem,
  type AIMessageItem,
} from "@/components/composed/message/message-list";
import { ThinkingStep } from "@/components/composed/thinking-process";
import { User, Bot } from "lucide-react";
import * as React from "react";

const messages = [
  {
    id: "1",
    role: "user" as const,
    content: "请分析一下这个问题",
    contentForCopy: "请分析一下这个问题",
    avatar: { icon: <User className="w-5 h-5" />, name: "用户", time: "10:30" },
  },
  {
    id: "2",
    role: "ai" as const,
    content: {
      type: "thinking",
      title: "分析完成",
      status: "completed" as const,
      duration: 12,
      content: "已从多维度分析问题,并生成详细方案与建议。",
      defaultOpen: true,
    },
    contentForCopy: "已从多维度分析问题,并生成详细方案与建议。",
    avatar: {
      icon: <Bot className="w-5 h-5" />,
      name: "AI 助手",
      time: "10:31",
    },
  },
] as (MessageItem | AIMessageItem)[];

function AIContent({ content }: { content: React.ReactNode }) {
  const c = content as {
    type?: string;
    title?: string;
    status?: string;
    duration?: number;
    content?: string;
    defaultOpen?: boolean;
  };
  if (c && typeof c === "object" && c.type === "thinking" && c.title) {
    const { type: _, ...props } = c;
    return (
      <ThinkingStep
        title={props.title}
        content={props.content}
        status={props.status as "completed"}
        duration={props.duration}
        defaultOpen={props.defaultOpen}
      />
    );
  }
  return <>{content}</>;
}

export function MessageListDemo() {
  return (
    <div className="h-[400px] w-full overflow-hidden">
      <MessageList
        messages={messages}
        showDefaultFeedback
        renderContent={(content, msg) =>
          msg.role === "ai" ? <AIContent content={content} /> : content
        }
      />
    </div>
  );
}

MessageList 组件是一个完整的聊天消息列表解决方案,集成了消息渲染、自动滚动、状态管理等功能,支持自定义渲染,适用于 AI 聊天界面、客服系统、在线问答等场景。

概述

  • 多种消息类型:支持用户消息(user)和 AI 消息(ai)
  • AI 消息状态:支持 idle、generating、failed 三种状态
  • 自动滚动:新消息时自动滚动到底部
  • 点击事件:支持消息点击回调
  • 空状态提示:无消息时显示友好提示
  • 头像支持:支持 AvatarHeader 组件,显示头像、名称、时间
  • 自定义渲染:支持自定义内容渲染器和完整消息渲染器
  • 无障碍支持:ARIA live regions 屏幕阅读器友好

快速开始

import { MessageList } from "@/registry/wuhan/composed/message/message-list";

interface Message {
  id: string;
  role: "user" | "ai";
  content: React.ReactNode;
}

export function Example() {
  const messages: Message[] = [
    { id: "1", role: "user", content: "你好!" },
    { id: "2", role: "ai", content: "你好,有什么可以帮助你的吗?" },
  ];

  return <MessageList messages={messages} />;
}

特性

  • 自动滚动:当消息列表更新时,自动滚动到最底部(可禁用)
  • 消息状态:AI 消息支持生成中状态,可显示自定义 loading 内容
  • 错误处理:AI 消息支持失败状态,可显示自定义错误内容
  • 消息点击:支持 onMessageClick 回调
  • 自定义反馈:支持 feedback 属性添加反馈区域
  • 头像支持:使用 AvatarHeader 组件,支持图片头像、图标头像、名称和时间
  • 自定义渲染
    • renderContent - 自定义消息内容渲染(如 Markdown)
    • renderMessage - 完全自定义消息项渲染

安装

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

代码演示

基本使用

最基础的消息列表,包含用户消息和 AI 消息。

用户10:30
请分析一下这个问题
AI 助手10:31
已从多维度分析问题,并生成详细方案与建议。
"use client";

import {
  MessageList,
  type MessageItem,
  type AIMessageItem,
} from "@/components/composed/message/message-list";
import { ThinkingStep } from "@/components/composed/thinking-process";
import { User, Bot } from "lucide-react";
import * as React from "react";

const messages = [
  {
    id: "1",
    role: "user" as const,
    content: "请分析一下这个问题",
    contentForCopy: "请分析一下这个问题",
    avatar: { icon: <User className="w-5 h-5" />, name: "用户", time: "10:30" },
  },
  {
    id: "2",
    role: "ai" as const,
    content: {
      type: "thinking",
      title: "分析完成",
      status: "completed" as const,
      duration: 12,
      content: "已从多维度分析问题,并生成详细方案与建议。",
      defaultOpen: true,
    },
    contentForCopy: "已从多维度分析问题,并生成详细方案与建议。",
    avatar: {
      icon: <Bot className="w-5 h-5" />,
      name: "AI 助手",
      time: "10:31",
    },
  },
] as (MessageItem | AIMessageItem)[];

function AIContent({ content }: { content: React.ReactNode }) {
  const c = content as {
    type?: string;
    title?: string;
    status?: string;
    duration?: number;
    content?: string;
    defaultOpen?: boolean;
  };
  if (c && typeof c === "object" && c.type === "thinking" && c.title) {
    const { type: _, ...props } = c;
    return (
      <ThinkingStep
        title={props.title}
        content={props.content}
        status={props.status as "completed"}
        duration={props.duration}
        defaultOpen={props.defaultOpen}
      />
    );
  }
  return <>{content}</>;
}

export function MessageListDemo() {
  return (
    <div className="h-[400px] w-full overflow-hidden">
      <MessageList
        messages={messages}
        showDefaultFeedback
        renderContent={(content, msg) =>
          msg.role === "ai" ? <AIContent content={content} /> : content
        }
      />
    </div>
  );
}

带输入的聊天界面

完整的聊天界面演示,包含输入框和消息发送功能。

用户10:30
请分析一下这个问题
AI 助手10:31
已从多维度分析问题,并生成详细方案与建议。
"use client";

import {
  MessageList,
  type MessageItem,
  type AIMessageItem,
} from "@/components/composed/message/message-list";
import { ThinkingStep } from "@/components/composed/thinking-process";
import { User, Bot } from "lucide-react";
import * as React from "react";

const messages = [
  {
    id: "1",
    role: "user" as const,
    content: "请分析一下这个问题",
    contentForCopy: "请分析一下这个问题",
    avatar: { icon: <User className="w-5 h-5" />, name: "用户", time: "10:30" },
  },
  {
    id: "2",
    role: "ai" as const,
    content: {
      type: "thinking",
      title: "分析完成",
      status: "completed" as const,
      duration: 12,
      content: "已从多维度分析问题,并生成详细方案与建议。",
      defaultOpen: true,
    },
    contentForCopy: "已从多维度分析问题,并生成详细方案与建议。",
    avatar: {
      icon: <Bot className="w-5 h-5" />,
      name: "AI 助手",
      time: "10:31",
    },
  },
] as (MessageItem | AIMessageItem)[];

function AIContent({ content }: { content: React.ReactNode }) {
  const c = content as {
    type?: string;
    title?: string;
    status?: string;
    duration?: number;
    content?: string;
    defaultOpen?: boolean;
  };
  if (c && typeof c === "object" && c.type === "thinking" && c.title) {
    const { type: _, ...props } = c;
    return (
      <ThinkingStep
        title={props.title}
        content={props.content}
        status={props.status as "completed"}
        duration={props.duration}
        defaultOpen={props.defaultOpen}
      />
    );
  }
  return <>{content}</>;
}

export function MessageListDemo() {
  return (
    <div className="h-[400px] w-full overflow-hidden">
      <MessageList
        messages={messages}
        showDefaultFeedback
        renderContent={(content, msg) =>
          msg.role === "ai" ? <AIContent content={content} /> : content
        }
      />
    </div>
  );
}

空状态

当没有消息时显示友好提示。

暂无消息
"use client";

import { MessageList } from "@/components/composed/message/message-list";

export function MessageListEmpty() {
  return (
    <div className="h-[400px] border rounded-lg overflow-hidden">
      <MessageList messages={[]} />
    </div>
  );
}

带头像的消息列表

使用图标头像显示用户和 AI 助手。

访客12:00
你好!
AI 助手12:01
你好!有什么可以帮助你的?
访客12:02
我想了解你的功能
"use client";

import { MessageList } from "@/components/composed/message/message-list";
import { User, Bot } from "lucide-react";

export function MessageListIconAvatar() {
  const messages = [
    {
      id: "1",
      role: "user" as const,
      content: "你好!",
      avatar: {
        icon: <User className="w-5 h-5" />,
        name: "访客",
        time: "12:00",
      },
    },
    {
      id: "2",
      role: "ai" as const,
      content: "你好!有什么可以帮助你的?",
      status: "idle" as const,
      avatar: {
        icon: <Bot className="w-5 h-5" />,
        name: "AI 助手",
        time: "12:01",
      },
    },
    {
      id: "3",
      role: "user" as const,
      content: "我想了解你的功能",
      avatar: {
        icon: <User className="w-5 h-5" />,
        name: "访客",
        time: "12:02",
      },
    },
  ];

  return (
    <div className="h-[400px] border rounded-lg overflow-hidden">
      <MessageList messages={messages} />
    </div>
  );
}

生成中状态

展示 AI 消息的生成中状态。

请帮我分析这段代码
正在分析...
"use client";

import { MessageList } from "@/components/composed/message/message-list";
import { useState } from "react";
import { LoadingDots } from "@/components/wuhan/blocks/message/message-01";
import type {
  MessageItem,
  AIMessageItem,
} from "@/components/composed/message/message-list";

export function MessageListGenerating() {
  const [messages, setMessages] = useState<(MessageItem | AIMessageItem)[]>([
    { id: "1", role: "user" as const, content: "请帮我分析这段代码" },
    {
      id: "2",
      role: "ai" as const,
      content: "",
      status: "generating" as const,
      generatingContent: (
        <div className="flex items-center gap-2 text-muted-foreground">
          <LoadingDots />
          <span>正在分析...</span>
        </div>
      ),
    },
  ]);

  // 模拟生成完成
  setTimeout(() => {
    setMessages((prev) =>
      prev.map((msg) =>
        msg.id === "2"
          ? {
              ...msg,
              status: "idle" as const,
              content:
                "分析完成!这段代码实现了一个计数器组件,使用了 React Hooks 来管理状态。",
            }
          : msg,
      ),
    );
  }, 3000);

  return (
    <div className="h-[300px] border rounded-lg overflow-hidden">
      <MessageList messages={messages} />
    </div>
  );
}

可重试的错误消息

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

请帮我查询数据

请求失败,请稍后重试

"use client";

import { MessageList } from "@/components/composed/message/message-list";
import { useState } from "react";
import { RefreshCw } from "lucide-react";
import type {
  MessageItem,
  AIMessageItem,
} from "@/components/composed/message/message-list";

export function MessageListRetryable() {
  const [messages, setMessages] = useState<(MessageItem | AIMessageItem)[]>([
    { id: "1", role: "user" as const, content: "请帮我查询数据" },
    {
      id: "2",
      role: "ai" as const,
      content: "",
      status: "failed" as const,
      errorContent: undefined,
    },
  ]);

  const handleRetry = (messageId: string) => {
    setMessages((prev) =>
      prev.map((msg) =>
        msg.id === messageId
          ? {
              ...msg,
              status: "generating" as const,
              content: "",
              errorContent: undefined,
            }
          : msg,
      ),
    );

    // 模拟重新请求
    setTimeout(() => {
      setMessages((prev) =>
        prev.map((msg) =>
          msg.id === messageId
            ? {
                ...msg,
                status: "idle" as const,
                content: "数据查询成功!这是查询结果...",
              }
            : msg,
        ),
      );
    }, 1500);
  };

  // 设置错误内容
  const secondMessage = messages[1];
  if (
    secondMessage &&
    "status" in secondMessage &&
    secondMessage.status === "failed" &&
    !secondMessage.errorContent
  ) {
    setMessages((prev) =>
      prev.map((msg, index) =>
        index === 1
          ? {
              ...msg,
              errorContent: (
                <div className="flex flex-col gap-2 p-3 bg-red-50 rounded-lg">
                  <p className="text-red-600">请求失败,请稍后重试</p>
                  <button
                    onClick={() => handleRetry(msg.id)}
                    className="flex items-center gap-2 px-3 py-1.5 bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors w-fit"
                  >
                    <RefreshCw className="w-4 h-4" />
                    重试
                  </button>
                </div>
              ),
            }
          : msg,
      ),
    );
  }

  return (
    <div className="h-[300px] border rounded-lg overflow-hidden">
      <MessageList messages={messages} />
    </div>
  );
}

API

MessageList

消息列表主组件。

Props

PropTypeDefaultDescription
messages(MessageItem | AIMessageItem | UserMessageItem)[][]消息列表数据
onMessageClick(message: MessageItem) => void-消息点击回调
classNamestring-额外的样式类名
autoScrollbooleantrue是否自动滚动到底部
renderContentMessageContentRenderer-自定义内容渲染器
renderMessageMessageRenderer-自定义消息渲染器

MessageItem

基础消息数据结构。

interface MessageItem {
  /** 唯一消息 ID */
  id: string;
  /** 消息角色 */
  role: "user" | "ai";
  /** 消息内容 */
  content: React.ReactNode;
  /** 时间戳 */
  timestamp?: number;
  /** 反馈内容 */
  feedback?: React.ReactNode;
  /** 头像配置(显示在消息上方) */
  avatar?: MessageAvatar;
}

MessageAvatar

头像配置(使用 AvatarHeader 组件)。

interface MessageAvatar {
  /** 头像图片 URL 或 ReactNode */
  src?: string | React.ReactNode;
  /** 头像图标 */
  icon?: React.ReactNode;
  /** 头像大小 */
  size?: "small" | "default" | "large";
  /** 名称 */
  name?: React.ReactNode;
  /** 时间 */
  time?: React.ReactNode;
}

AIMessageItem

AI 消息额外属性。

interface AIMessageItem extends MessageItem {
  role: "ai";
  /** AI 消息状态 */
  status?: "idle" | "generating" | "failed";
  /** 生成中自定义内容 */
  generatingContent?: React.ReactNode;
  /** 失败自定义内容 */
  errorContent?: React.ReactNode;
}

UserMessageItem

用户消息类型。

interface UserMessageItem extends MessageItem {
  role: "user";
}

MessageContentRenderer

自定义内容渲染器类型。

type MessageContentRenderer = (
  content: React.ReactNode,
  message: MessageItem | AIMessageItem | UserMessageItem,
) => React.ReactNode;

MessageRenderer

自定义消息渲染器类型(完全自定义)。

type MessageRenderer = (
  message: MessageItem | AIMessageItem | UserMessageItem,
  defaultRender: () => React.ReactNode,
) => React.ReactNode;

Example

import { MessageList, type AIMessageItem } from "@/registry/wuhan/composed/message/message-list";

function ChatExample() {
  const messages: (MessageItem | AIMessageItem)[] = [
    {
      id: "1",
      role: "user",
      content: "你好,请帮我写一个函数",
    },
    {
      id: "2",
      role: "ai",
      content: "当然可以,请告诉我具体需求",
      status: "idle",
    },
    {
      id: "3",
      role: "ai",
      content: "",
      status: "generating",
      generatingContent: <LoadingDots />,
    },
  ];

  return (
    <MessageList
      messages={messages}
      onMessageClick={(msg) => console.log("点击消息:", msg.id)}
      autoScroll={true}
    />
  );
}

自定义渲染

MessageList 提供了两种自定义渲染方式,让你可以灵活地定制消息的展示效果。

renderContent - 自定义内容渲染

使用 renderContent 可以自定义消息内容的渲染方式,常用于:

  • 使用 Markdown 组件渲染 AI 消息
  • 根据消息类型使用不同的渲染组件
  • 对特殊内容格式进行自定义处理
import { MessageList } from "@/registry/wuhan/composed/message/message-list";
import Markdown from "@/registry/wuhan/composed/markdown";

function ChatWithMarkdown() {
  const messages = [
    {
      id: "1",
      role: "user",
      content: "请解释一下 React",
    },
    {
      id: "2",
      role: "ai",
      content: "## React 简介\nReact 是一个用于构建用户界面的 JavaScript 库...",
    },
  ];

  return (
    <MessageList
      messages={messages}
      renderContent={(content, msg) => {
        // AI 消息使用 Markdown 渲染
        if (msg.role === "ai" && typeof content === "string") {
          return <Markdown>{content}</Markdown>;
        }
        return content;
      }}
    />
  );
}

renderMessage - 完全自定义消息渲染

使用 renderMessage 可以完全自定义消息项的渲染,包括:

  • 为特定消息添加特殊样式或装饰
  • 为失败消息添加重试按钮
  • 根据消息内容显示不同的布局
import { MessageList, type AIMessageItem } from "@/registry/wuhan/composed/message/message-list";
import { Button } from "@/registry/wuhan/ui/button";

function CustomMessageList() {
  return (
    <MessageList
      messages={messages}
      renderMessage={(message, defaultRender) => {
        const aiMsg = message as AIMessageItem;

        // 为失败消息添加重试按钮
        if (aiMsg.status === "failed") {
          return (
            <div className="relative group">
              {defaultRender()}
              <Button
                className="absolute -top-2 -right-2 opacity-0 group-hover:opacity-100"
                onClick={() => retryMessage(message.id)}
              >
                重试
              </Button>
            </div>
          );
        }

        // 为第一条消息添加特殊样式
        if (message.id === messages[0]?.id) {
          return (
            <div className="transform scale-105 transition-transform">
              {defaultRender()}
            </div>
          );
        }

        return defaultRender();
      }}
    />
  );
}

结合使用

你可以同时使用 renderContentrenderMessage

<MessageList
  messages={messages}
  renderContent={(content, msg) => {
    // 自定义内容渲染
    if (msg.role === "ai") {
      return <Markdown>{content}</Markdown>;
    }
    return content;
  }}
  renderMessage={(message, defaultRender) => {
    // 自定义消息项渲染
    if (message.id === "special") {
      return (
        <div className="border-l-4 border-primary pl-4">
          {defaultRender()}
        </div>
      );
    }
    return defaultRender();
  }}
/>

使用场景

  • AI 聊天应用:ChatGPT、Claude 等对话界面
  • 客服系统:在线客服、智能问答
  • 协作工具:团队沟通、讨论区
  • 教育平台:在线答疑、学习辅导
  • 代码助手:编程辅助、技术问答

最佳实践

  1. 状态管理:使用 generating 状态显示 loading,内容为空时渲染自定义 loading
  2. 错误恢复:失败状态提供重试按钮或重新生成选项
  3. 流式更新:配合 SSE/WebSocket 实现流式内容渲染
  4. 消息历史:保存消息历史,支持加载更多和分页
  5. 用户体验:新消息时自动滚动,但允许用户取消自动滚动
  6. 内容渲染:使用 renderContent 配合 Markdown 组件渲染 AI 消息
  7. 特殊消息:使用 renderMessage 为特定消息添加特殊效果
  8. 头像使用:使用 avatar 属性为每条消息配置独立头像,或设置统一的默认头像

注意事项

  • autoScroll 默认为 true,新消息时自动滚动到底部
  • generatingContent 只在 status 为 "generating" 时显示
  • errorContent 只在 status 为 "failed" 时显示
  • renderContentrenderMessage 可以结合使用
  • renderMessage 的第二个参数 defaultRender 返回默认渲染结果
  • 消息列表高度需要自行设置,建议使用固定高度或 flex 布局
  • 头像显示在消息上方,使用 AvatarHeader 组件实现

与原语组件的关系

MessageList 基于以下组件构建:

  • AIMessage - AI 消息组件(来自 @/registry/wuhan/composed/message
  • UserMessage - 用户消息组件(来自 @/registry/wuhan/composed/message
  • AvatarHeader - 头像头部组件(来自 @/registry/wuhan/composed/avatar-header

这些组件又基于原语组件构建:

  • MessageAIPrimitive - AI 消息容器原语
  • MessageUserPrimitive - 用户消息容器原语
  • AvatarHeaderPrimitive - 头像头部原语

如果需要更深入的定制,可以直接使用 AIMessage、UserMessage 和 AvatarHeader 组件。

带头像的消息列表

import { MessageList } from "@/registry/wuhan/composed/message/message-list";

function MessageListWithAvatar() {
  const messages = [
    {
      id: "1",
      role: "user",
      content: "你好,我想咨询一下产品",
      avatar: {
        src: "https://example.com/avatar.jpg",
        name: "张三",
        time: "10:30",
      },
    },
    {
      id: "2",
      role: "ai",
      content: "你好!很高兴为您服务。",
      avatar: {
        src: "https://example.com/ai-avatar.jpg",
        name: "智能客服",
        time: "10:31",
      },
    },
  ];

  return <MessageList messages={messages} />;
}

使用图标头像

import { MessageList } from "@/registry/wuhan/composed/message/message-list";
import { User, Bot } from "lucide-react";

function MessageListWithIconAvatar() {
  const messages = [
    {
      id: "1",
      role: "user",
      content: "你好!",
      avatar: {
        icon: <User className="w-5 h-5" />,
        name: "访客",
        time: "12:00",
      },
    },
    {
      id: "2",
      role: "ai",
      content: "你好!有什么可以帮助你的?",
      avatar: {
        icon: <Bot className="w-5 h-5" />,
        name: "AI 助手",
        time: "12:01",
      },
    },
  ];

  return <MessageList messages={messages} />;
}

不同尺寸头像

import { MessageList } from "@/registry/wuhan/composed/message/message-list";
import { User } from "lucide-react";

function MessageListAvatarSizes() {
  const messages = [
    {
      id: "1",
      role: "user",
      content: "小头像",
      avatar: {
        icon: <User className="w-3 h-3" />,
        size: "small",
        name: "小",
        time: "12:00",
      },
    },
    {
      id: "2",
      role: "ai",
      content: "默认尺寸头像",
      avatar: {
        icon: <Bot className="w-5 h-5" />,
        size: "default",
        name: "默认",
        time: "12:01",
      },
    },
    {
      id: "3",
      role: "user",
      content: "大头像",
      avatar: {
        icon: <User className="w-7 h-7" />,
        size: "large",
        name: "大",
        time: "12:02",
      },
    },
  ];

  return <MessageList messages={messages} />;
}

自定义生成中状态

import { MessageList, type AIMessageItem } from "@/registry/wuhan/composed/message/message-list";
import { LoadingDots } from "@/registry/wuhan/blocks/message/message-01";

function CustomGeneratingDemo() {
  const [messages, setMessages] = useState<MessageItem[]>([
    { id: "1", role: "user", content: "请帮我分析这段代码" },
  ]);

  const addAiMessage = () => {
    const newMessage: AIMessageItem = {
      id: Date.now().toString(),
      role: "ai",
      content: "",
      status: "generating",
      generatingContent: (
        <div className="flex items-center gap-2 text-[var(--Text-text-secondary)]">
          <LoadingDots />
          <span>正在分析...</span>
        </div>
      ),
    };
    setMessages([...messages, newMessage]);

    // 模拟生成完成
    setTimeout(() => {
      setMessages((prev) =>
        prev.map((msg) =>
          msg.id === newMessage.id
            ? { ...msg, status: "idle", content: "分析完成!这是一段很长的分析结果..." }
            : msg
        )
      );
    }, 2000);
  };

  return (
    <div className="space-y-4">
      <MessageList messages={messages} />
      <button onClick={addAiMessage}>添加 AI 消息</button>
    </div>
  );
}

可重试的错误状态

import { MessageList, type AIMessageItem } from "@/registry/wuhan/composed/message/message-list";
import { Button } from "@/registry/wuhan/ui/button";
import { RefreshCw } from "lucide-react";

function RetryableMessageList() {
  const [messages, setMessages] = useState<(MessageItem | AIMessageItem)[]>([
    { id: "1", role: "user", content: "请帮我查询数据" },
  ]);

  const handleRetry = (messageId: string) => {
    setMessages((prev) =>
      prev.map((msg) =>
        msg.id === messageId
          ? { ...msg, status: "generating", content: "", errorContent: undefined }
          : msg
      )
    );

    // 模拟重新请求
    setTimeout(() => {
      setMessages((prev) =>
        prev.map((msg) =>
          msg.id === messageId
            ? { ...msg, status: "idle", content: "数据查询成功!" }
            : msg
        )
      );
    }, 1500);
  };

  const addFailedMessage = () => {
    const newMessage: AIMessageItem = {
      id: Date.now().toString(),
      role: "ai",
      content: "",
      status: "failed",
      errorContent: (
        <div className="flex flex-col gap-2 p-3 bg-[var(--Container-bg-error-light)] rounded-lg">
          <p className="text-[var(--Text-text-error)]">请求失败,请稍后重试</p>
          <Button
            variant="outline"
            size="sm"
            onClick={() => handleRetry(newMessage.id)}
          >
            <RefreshCw className="w-4 h-4 mr-1" />
            重试
          </Button>
        </div>
      ),
    };
    setMessages([...messages, newMessage]);
  };

  return (
    <div className="space-y-4">
      <MessageList messages={messages} />
      <button onClick={addFailedMessage}>添加失败消息</button>
    </div>
  );
}

完整的聊天应用

import { useState } from "react";
import {
  MessageList,
  type MessageItem,
  type AIMessageItem,
} from "@/registry/wuhan/composed/message/message-list";
import { Button } from "@/registry/wuhan/ui/button";
import { Send } from "lucide-react";

interface ChatMessage extends MessageItem {
  status?: "idle" | "generating" | "failed";
}

export function ChatApp() {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [inputValue, setInputValue] = useState("");

  const handleSend = () => {
    if (!inputValue.trim()) return;

    // 添加用户消息
    const userMsg: ChatMessage = {
      id: Date.now().toString(),
      role: "user",
      content: inputValue,
      avatar: {
        src: "https://api.dicebear.com/7.x/avataaars/svg?seed=user1",
        name: "用户",
        time: new Date().toLocaleTimeString("zh-CN", {
          hour: "2-digit",
          minute: "2-digit",
        }),
      },
    };
    setMessages((prev) => [...prev, userMsg]);
    setInputValue("");

    // 模拟 AI 回复
    setTimeout(() => {
      const aiMsg: ChatMessage = {
        id: (Date.now() + 1).toString(),
        role: "ai",
        content: "",
        status: "generating",
        avatar: {
          src: "https://api.dicebear.com/7.x/bottts/svg?seed=ai",
          name: "AI 助手",
          time: new Date().toLocaleTimeString("zh-CN", {
            hour: "2-digit",
            minute: "2-digit",
          }),
        },
      };
      setMessages((prev) => [...prev, aiMsg]);

      // 模拟生成完成
      setTimeout(() => {
        setMessages((prev) =>
          prev.map((msg) =>
            msg.id === aiMsg.id
              ? { ...msg, status: "idle", content: "这是 AI 的回复!" }
              : msg
          )
        );
      }, 2000);
    }, 500);
  };

  return (
    <div className="flex flex-col h-[600px] border rounded-lg overflow-hidden">
      {/* 消息区域 */}
      <div className="flex-1 overflow-hidden">
        <MessageList
          messages={messages}
          onMessageClick={(msg) => console.log("点击:", msg.id)}
        />
      </div>

      {/* 输入区域 */}
      <div className="border-t p-4 bg-[var(--Container-bg-neutral)]">
        <div className="flex gap-2">
          <input
            type="text"
            value={inputValue}
            onChange={(e) => setInputValue(e.target.value)}
            onKeyDown={(e) => e.key === "Enter" && handleSend()}
            placeholder="输入消息..."
            className="flex-1 px-4 py-2 border rounded-lg"
          />
          <Button onClick={handleSend}>
            <Send className="w-4 h-4 mr-1" />
            发送
          </Button>
        </div>
      </div>
    </div>
  );
}

与原语组件的关系

MessageList 基于以下组件构建:

  • AIMessage - AI 消息组件(来自 @/registry/wuhan/composed/message
  • UserMessage - 用户消息组件(来自 @/registry/wuhan/composed/message
  • AvatarHeader - 头像头部组件(来自 @/registry/wuhan/composed/avatar-header

这些组件又基于原语组件构建:

  • MessageAIPrimitive - AI 消息容器原语
  • MessageUserPrimitive - 用户消息容器原语
  • AvatarHeaderPrimitive - 头像头部原语

如果需要更深入的定制,可以直接使用 AIMessage、UserMessage 和 AvatarHeader 组件。