shadcn/ui 加入聊天组件:用可组合原语搭建对话界面

2026-08-17 38 预计阅读时间: 1 分钟
来源: infoq.com AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:9 分钟

聊天界面看似只是“消息列表加输入框”,真正实现时却会遇到自动滚动、角色布局、流式更新、长内容渲染和无障碍语义等问题。shadcn/ui 新增面向对话场景的组件,其中包括 MessageScrollerMessage,把这些重复出现的界面能力拆成可以组合、替换和继续定制的原语。

组件不是黑盒,而是可修改的工程起点

这次更新延续了 shadcn/ui 的核心思路:开发者获得的是能够纳入项目并继续修改的组件代码,而不是只能通过有限属性控制的封闭组件。

在聊天场景里,这一点尤其重要。不同产品对消息的呈现差异很大:

  • 客服系统需要区分客户、坐席和系统事件;
  • AI 助手需要展示 Markdown、代码、工具调用与流式生成状态;
  • 团队协作应用可能还要加入引用、附件、表情回应和线程;
  • 审核或运维工具则更看重时间戳、错误状态和可追踪的消息 ID。

Message 可以承担单条消息的结构与角色样式,MessageScroller 则负责消息区域的滚动行为。业务层仍然保存消息状态、调用接口和处理流式数据。这样修改气泡外观时,不必碰请求逻辑;更换模型或后端协议时,也不必重写整个消息列表。

Headless 支持让行为和外观分开演进

摘要提到这批对话能力支持 headless 组件。对实际项目而言,headless 的价值不是“没有样式”,而是让行为、状态和 DOM 表达不被某一套视觉方案锁死。

团队可以在相同的交互逻辑上构建不同界面:桌面端使用紧凑的信息密度,移动端增加触控间距,嵌入式助手改成侧边栏布局。设计系统升级时,消息数据模型和发送流程仍可保持稳定。

这种组合方式也有边界。组件不会替业务层决定:

  • 哪些新消息应该触发自动滚动;
  • 用户向上阅读历史记录时是否保持当前位置;
  • Markdown 和 HTML 如何清洗;
  • 流式响应中断后怎样重试;
  • 对话记录是否包含敏感数据,以及如何保存。

这些仍然需要产品和工程团队明确约束。

可以这样实践:组装一个最小聊天面板

下面是一个可改造的 React/TypeScript 示例。假设项目已经配置 shadcn/ui,并将新组件放在 @/components/ui/message@/components/ui/message-scroller。组件的实际导出名称和属性可能随项目版本不同,请按本地生成的代码调整 import 和 props。

"use client";

import { FormEvent, useState } from "react";
import { Message } from "@/components/ui/message";
import { MessageScroller } from "@/components/ui/message-scroller";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";

type ChatMessage = {
  id: string;
  role: "user" | "assistant";
  content: string;
};

const initialMessages: ChatMessage[] = [
  {
    id: "welcome",
    role: "assistant",
    content: "你好,我可以帮你检查这次发布的变更。",
  },
];

export default function ChatPanel() {
  const [messages, setMessages] = useState(initialMessages);
  const [draft, setDraft] = useState("");

  function submit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const content = draft.trim();
    if (!content) return;

    setMessages((current) => [
      ...current,
      { id: crypto.randomUUID(), role: "user", content },
      {
        id: crypto.randomUUID(),
        role: "assistant",
        content: `已收到:${content}`,
      },
    ]);
    setDraft("");
  }

  return (
    <section className="mx-auto grid h-[560px] max-w-2xl grid-rows-[1fr_auto] border">
      <MessageScroller className="min-h-0 p-4" aria-live="polite">
        <div className="space-y-4">
          {messages.map((message) => (
            <Message key={message.id} role={message.role}>
              <p className="whitespace-pre-wrap break-words">{message.content}</p>
            </Message>
          ))}
        </div>
      </MessageScroller>

      <form onSubmit={submit} className="flex gap-2 border-t p-3">
        <Input
          value={draft}
          onChange={(event) => setDraft(event.target.value)}
          aria-label="消息内容"
          placeholder="输入消息"
          autoComplete="off"
        />
        <Button type="submit" disabled={!draft.trim()}>
          发送
        </Button>
      </form>
    </section>
  );
}

这个示例故意把业务逻辑保持在组件外部。接入真实服务时,可以把模拟回复替换成 API 调用,但消息数组仍然作为唯一渲染来源:

const response = await fetch("/api/chat", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ messages }),
});

if (!response.ok) {
  throw new Error(`Chat request failed: ${response.status}`);
}

const reply = (await response.json()) as { content: string };
setMessages((current) => [
  ...current,
  {
    id: crypto.randomUUID(),
    role: "assistant",
    content: reply.content,
  },
]);

生产环境还应增加请求中的禁用状态、取消请求、错误消息以及重复提交保护。若后端采用 SSE 或其他流式协议,不要为每个文本片段创建一条新消息;应更新同一条助手消息的 content,避免列表和辅助技术被大量节点淹没。

自动滚动需要尊重用户当前的位置

聊天组件最容易被低估的是滚动策略。收到新消息就无条件滚到底部,会打断正在阅读历史内容的用户。更稳妥的规则是:只有当用户原本接近底部,或者新消息由用户本人发送时,才自动跟随最新内容。

同时要测试三种情况:短回复、一次性插入很长的代码块,以及持续数十秒的流式回复。消息高度变化时,滚动容器应保持稳定;“跳到最新消息”按钮也不应遮挡输入框。

采用前的检查清单

把新组件接入现有项目时,可以按以下顺序评估:

  1. 先确认 Message 的角色模型能否覆盖用户、助手、系统和工具消息。
  2. 检查 MessageScroller 在历史记录加载、流式更新和移动端键盘弹出时的行为。
  3. 对 Markdown、链接和 HTML 做明确的转义或清洗,不能直接信任模型输出。
  4. 为发送中、失败、取消和重试设计可观察状态。
  5. 使用键盘和屏幕阅读器验证输入焦点、消息播报与错误提示。
  6. 将业务协议留在数据层,避免把 API 请求和供应商字段写进视觉组件。

shadcn/ui 的这次扩展并不是替开发者交付完整聊天产品,而是提供更合适的构建单位。它减少了消息结构和滚动容器的重复工作,同时保留了修改代码、替换样式以及接入自有状态管理的空间。对于已经使用 shadcn/ui 的团队,适合先在一个边界清晰的聊天面板中试用,再根据流式响应、历史加载和无障碍测试结果决定是否扩大采用范围。


相关推荐