AI Elements
基于 shadcn/ui 的 AI 聊天界面可组合组件库,支持配合 useStream 使用
AI Elements 是一个基于 shadcn/ui 的可组合组件库,专为 AI 聊天界面打造。诸如 Conversation、Message、Tool、Reasoning 以及 PromptInput 等组件旨在直接放入任何 React 项目中,仅需极少的胶水代码即可与 stream.messages 连接。
TIP
克隆并运行完整 AI Elements 示例,在实际运行的项目中查看工具调用渲染、推理展示、流式传输消息等功能。
工作原理
- 以源码文件形式安装组件: AI Elements 通过 CLI 分发,将组件直接添加到你的项目中(shadcn/ui 注册表风格)
- 将消息映射到组件: 遍历
stream.messages,将HumanMessage实例渲染为用户气泡,并将AIMessage实例渲染为助手响应 - 组合更丰富的 UI: 将工具调用包装在
<Tool>中,推理过程包装在<Reasoning>中,并将所有内容包装在<Conversation>中以进行滚动管理
安装
通过 CLI 安装 AI Elements 组件。它们会作为可编辑的源码文件添加到你的项目中:
bash
npm install @langchain/react
npx ai-elements@latest add conversation message prompt-input tool reasoning suggestion接入 useStream
直接从 stream.messages 渲染 AI Elements 组件。每个 LangChain BaseMessage 对应一个组件:
tsx
import { useStream } from "@langchain/react";
import { HumanMessage, AIMessage } from "langchain";
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import {
Message,
MessageContent,
MessageResponse,
} from "@/components/ai-elements/message";
import {
Tool,
ToolHeader,
ToolContent,
ToolInput,
ToolOutput,
} from "@/components/ai-elements/tool";
import {
Reasoning,
ReasoningTrigger,
ReasoningContent,
} from "@/components/ai-elements/reasoning";
import {
PromptInput,
PromptInputBody,
PromptInputTextarea,
PromptInputFooter,
PromptInputSubmit,
} from "@/components/ai-elements/prompt-input";
function getReasoningText(msg: AIMessage) {
return msg.contentBlocks.find((block) => block.type === "reasoning")?.reasoning ?? "";
}
function getTextContent(msg: AIMessage) {
return msg.text;
}
function getToolCalls(msg: AIMessage) {
return (msg.tool_calls ?? []).map((tc) => ({
id: tc.id,
name: tc.name,
args: tc.args,
state: "input-available" as const,
}));
}
export function Chat() {
const stream = useStream({
apiUrl: "http://localhost:2024",
assistantId: "ai_elements",
});
return (
<div className="flex flex-col h-dvh">
<Conversation className="flex-1">
<ConversationContent>
{stream.messages.map((msg, i) => {
if (HumanMessage.isInstance(msg)) {
return (
<Message key={i} from="user">
<MessageContent>{msg.text}</MessageContent>
</Message>
);
}
if (AIMessage.isInstance(msg)) {
return (
<div key={i}>
{/* 推理块(当模型输出思考 token 时显示) */}
<Reasoning>
<ReasoningTrigger />
<ReasoningContent>{getReasoningText(msg)}</ReasoningContent>
</Reasoning>
{/* 内联工具调用及输入/输出展示 */}
{getToolCalls(msg).map((tc) => (
<Tool key={tc.id} defaultOpen>
<ToolHeader type={`tool-${tc.name}`} state={tc.state} />
<ToolContent>
<ToolInput input={tc.args} />
{tc.output && (
<ToolOutput output={tc.output} errorText={undefined} />
)}
</ToolContent>
</Tool>
))}
{/* 流式文本响应 */}
<Message from="assistant">
<MessageContent>
<MessageResponse>{getTextContent(msg)}</MessageResponse>
</MessageContent>
</Message>
</div>
);
}
})}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
<PromptInput
onSubmit={({ text }) =>
stream.submit({ messages: [{ type: "human", content: text }] })
}
>
<PromptInputBody>
<PromptInputTextarea placeholder="Ask me something..." />
</PromptInputBody>
<PromptInputFooter>
<PromptInputSubmit
status={stream.isLoading ? "streaming" : "ready"}
/>
</PromptInputFooter>
</PromptInput>
</div>
);
}最佳实践
- 自由编辑源码文件: 组件直接置于你的项目中,而非作为外部包依赖,因此你无需 fork 即可随意修改任何内容
- 使用
MessageResponse处理流式传输: 它可以正确处理流式的分块 token;避免在流式传输过程中直接渲染原始消息内容 - 用
Conversation包装:Conversation组件负责管理滚动行为,以便新消息能自动滚动进入视野 - 使用
isInstance进行守卫: 使用HumanMessage.isInstance(msg)和AIMessage.isInstance(msg),而不是检查msg.getType(),以获得正确的 TypeScript 类型收窄
