外观
AI Elements 是一个可组合的、基于 shadcn/ui 的组件库,专为 AI 聊天界面而构建。Conversation、Message、Tool、Reasoning 和 PromptInput 等组件设计为可直接放入任何 React 项目,并以最少的粘合代码连接到 stream.messages。
import { ExampleEmbed } from "/snippets/example-embed.jsx"
TIP
克隆并运行完整的 AI Elements 示例,以在一个可运行的项目中查看工具调用渲染、推理展示、流式输出消息等更多内容。
工作原理
- 将组件作为源文件安装: AI Elements 通过 CLI 分发,可直接将组件添加到你的项目中(shadcn/ui registry 风格)
- 将消息映射到组件: 遍历
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 (
<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 (
{/* Reasoning block (shows when model emits thinking tokens) */}
<Reasoning>
<ReasoningTrigger />
<ReasoningContent>{getReasoningText(msg)}</ReasoningContent>
</Reasoning>
{/* Inline tool calls with input/output display */}
{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>
))}
{/* Streamed text response */}
<Message from="assistant">
<MessageContent>
<MessageResponse>{getTextContent(msg)}</MessageResponse>
</MessageContent>
</Message>
);
}
})}
</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>
);
}最佳实践
- 自由编辑源文件: 组件随项目一起分发,而不是作为外部包依赖,因此你可以在不 fork 的情况下更改任何内容
- 使用
MessageResponse进行流式输出: 它能正确处理流式传输的增量 token;避免在流式输出期间直接渲染原始消息内容 - 包裹在
Conversation中:Conversation组件管理滚动行为,使新消息自动滚动到视野中 - 用
isInstance进行类型守卫: 使用HumanMessage.isInstance(msg)和AIMessage.isInstance(msg),而不是检查msg.getType(),以获得正确的 TypeScript 类型收窄