@so2liu/ma-chat
v0.2.5
Published
A flexible React chat UI library with multi-level DX support for building AI-powered chat interfaces
Maintainers
Readme
@so2liu/ma-chat
一个灵活的 React 聊天 UI 库,支持多层次的开发者体验(DX),专为构建 AI 驱动的聊天界面设计。
特性
- 多层次 DX:从零配置组件到完全可定制的 headless core
- 流式支持:实时 SSE 流式传输,带 token 聚合
- 丰富的消息类型:支持事件、工具调用、规划和自定义卡片
- 可扩展渲染:卡片注册表和代码块渲染器注册表,支持自定义
- 组件库:消息列表、事件卡片、模态框等
- TypeScript 优先:完整的类型安全和 IntelliSense 支持
安装
pnpm add @so2liu/ma-chat
# 或
npm install @so2liu/ma-chat入口点
| 入口点 | 描述 | 适用场景 |
|--------|------|----------|
| @so2liu/ma-chat | 完整库(包含 useMAClient) | 大多数场景 |
| @so2liu/ma-chat/api | 类型安全的 API 客户端 | 仅需 API 客户端时 |
| @so2liu/ma-chat/core | Headless 核心(无 React 依赖) | 非 React 框架或自定义实现 |
| @so2liu/ma-chat/react | 仅 React hooks | 只需要状态管理 |
| @so2liu/ma-chat/components | UI 组件 | 自定义布局 |
| @so2liu/ma-chat/styles.css | 样式文件 | 需要默认样式时 |
前置条件:创建会话
使用 useMAClient hook 调用 API 创建会话。该 hook 必须在 ChatProvider 内部使用:
import { useMAClient } from '@so2liu/ma-chat';
function AgentSelector({ onSessionCreated }: { onSessionCreated: (id: string) => void }) {
const { createSession, getAgentGroupTypes } = useMAClient();
const handleSelect = async (agentGroupId: string) => {
const { data } = await createSession({
body: {
agent_group_id: agentGroupId, // 智能体组 ID,如 "shopping"
title: 'My Chat Session', // 可选:会话标题
},
});
if (data?.session_id) {
onSessionCreated(data.session_id);
}
};
// ...
}API 请求参数:
| 参数 | 类型 | 必填 | 描述 |
|------|------|------|------|
| agent_group_id | string | 是 | 智能体组 ID |
| title | string | 否 | 会话标题,默认 "untitled conversation" |
| session_id | string | 否 | 自定义会话 ID,不提供则自动生成 |
| user_id | string | 否 | 用户 ID,默认 "placeholder" |
注意:
useMAClient必须在ChatProvider内部使用,否则会抛出错误。
使用指南
Level 1: 快速开始(初学者)
最简单的使用方式,使用 ChatProvider 和 ChatWidget:
import { useState } from 'react';
import { ChatProvider, ChatWidget, useMAClient } from '@so2liu/ma-chat';
import '@so2liu/ma-chat/styles.css';
const BACKEND_URL = 'http://localhost:8000';
// 选择智能体并创建会话
function AgentSelector({ onSessionCreated }: { onSessionCreated: (id: string) => void }) {
const { createSession } = useMAClient();
const [loading, setLoading] = useState(false);
const handleStart = async () => {
setLoading(true);
const { data } = await createSession({
body: { agent_group_id: 'shopping' },
});
if (data?.session_id) {
onSessionCreated(data.session_id);
}
setLoading(false);
};
return (
<button onClick={handleStart} disabled={loading}>
{loading ? '创建中...' : '开始对话'}
</button>
);
}
// 主应用
function App() {
const [sessionId, setSessionId] = useState<string | null>(null);
return (
<ChatProvider backendUrl={BACKEND_URL}>
{sessionId ? (
<ChatWidget sessionId={sessionId} />
) : (
<AgentSelector onSessionCreated={setSessionId} />
)}
</ChatProvider>
);
}关键点:
ChatWidget和useMAClient都必须在ChatProvider内使用backendUrl配置在ChatProvider上,useMAClient会自动获取- 所有 API 调用都有完整的类型提示
适用场景:快速原型、简单集成、不需要自定义 UI
Level 2: 自定义布局(中级)
使用 hooks 和预置组件构建自定义布局:
import { useState } from 'react';
import {
ChatProvider,
useChat,
useMAClient,
VerboseMessageList,
filterNoiseEvents
} from '@so2liu/ma-chat';
import '@so2liu/ma-chat/styles.css';
const BACKEND_URL = 'http://localhost:8000';
function ChatContent({ sessionId }: { sessionId: string }) {
const {
messages,
sendMessage,
isStreaming,
streamContent,
toolCallStreams
} = useChat({
sessionId,
backendUrl: BACKEND_URL,
});
// 过滤噪音事件(如 llm_call_start 等)
const displayMessages = filterNoiseEvents(messages);
return (
<div className="flex flex-col h-screen">
{/* 自定义头部 */}
<header className="p-4 border-b">
<h1>AI Assistant</h1>
</header>
{/* 消息列表 */}
<main className="flex-1 overflow-auto">
<VerboseMessageList
messages={displayMessages}
currentStreamContent={streamContent}
currentToolCallStreams={toolCallStreams}
isStreaming={isStreaming}
sessionId={sessionId}
fileBaseUrl={`${BACKEND_URL}/agent/v1/file`}
/>
</main>
{/* 自定义输入区域 */}
<footer className="p-4 border-t">
<input
type="text"
placeholder="输入消息..."
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
sendMessage(e.currentTarget.value);
e.currentTarget.value = '';
}
}}
className="w-full p-2 border rounded"
/>
</footer>
</div>
);
}
// 智能体选择器
function AgentSelector({ onSessionCreated }: { onSessionCreated: (id: string) => void }) {
const { createSession } = useMAClient();
const handleStart = async () => {
const { data } = await createSession({
body: { agent_group_id: 'shopping' },
});
if (data?.session_id) {
onSessionCreated(data.session_id);
}
};
return <button onClick={handleStart}>开始对话</button>;
}
function App() {
const [sessionId, setSessionId] = useState<string | null>(null);
return (
<ChatProvider backendUrl={BACKEND_URL}>
{sessionId ? (
<ChatContent sessionId={sessionId} />
) : (
<AgentSelector onSessionCreated={setSessionId} />
)}
</ChatProvider>
);
}可用组件:
VerboseMessageList- 线性消息展示CompactModeView- 树形消息展示EventRenderer- 事件类型路由CustomMarkdown- Markdown 渲染器
可用 Hooks:
useChat()- 主要 hook,管理聊天状态useMessages()- 订阅消息列表useIsStreaming()- 订阅流式状态useStreamContent()- 获取当前流式内容useToolCallStreams()- 获取工具调用流
Level 3: 自定义卡片和代码块(高级)
注册自定义卡片组件和代码块渲染器:
自定义卡片
import {
registerCard,
type CardComponentProps
} from '@so2liu/ma-chat';
// 定义自定义卡片组件
function MyProductCard({ card, sendMessage }: CardComponentProps) {
const { name, price, image } = card.payload as {
name: string;
price: number;
image: string;
};
return (
<div className="border rounded-lg p-4">
<img src={image} alt={name} className="w-full h-48 object-cover" />
<h3 className="text-lg font-bold">{name}</h3>
<p className="text-xl text-primary">{price}</p>
<button
onClick={() => sendMessage?.(`购买 ${name}`)}
className="mt-2 w-full bg-primary text-white py-2 rounded"
>
立即购买
</button>
</div>
);
}
// 在应用启动时注册
registerCard('product-card', MyProductCard);AI 返回以下格式时会自动渲染:
```card+json
{
"type": "product-card",
"payload": {
"name": "智能手表",
"price": 999,
"image": "https://example.com/watch.jpg"
}
}
```自定义代码块渲染器
import { useState, useEffect } from 'react';
import {
registerCodeBlockRenderer,
type CodeBlockRendererProps
} from '@so2liu/ma-chat';
// 自定义 PlantUML 渲染器
function PlantUmlRenderer({ code, isStreaming }: CodeBlockRendererProps) {
const [svg, setSvg] = useState<string>('');
useEffect(() => {
if (!isStreaming) {
// 调用 PlantUML 服务渲染
renderPlantUml(code).then(setSvg);
}
}, [code, isStreaming]);
if (isStreaming) {
return <pre className="bg-muted p-4 rounded">{code}</pre>;
}
return <div dangerouslySetInnerHTML={{ __html: svg }} />;
}
// 注册渲染器
registerCodeBlockRenderer('plantuml', PlantUmlRenderer);内置特殊代码块语言:
card+json- 卡片渲染mermaid- Mermaid 图表html- HTML 预览json+echarts/echarts+json- ECharts 图表
Level 4: 完全控制(专家)
使用 headless core 在任意框架中构建:
import {
ChatClient,
EventTransformer,
TokenGrouper
} from '@so2liu/ma-chat/core';
// 创建客户端(无 React 依赖)
const client = new ChatClient({
backendUrl: 'https://api.example.com'
});
// Token 聚合器(优化流式渲染)
const tokenGrouper = new TokenGrouper({
onFlush: (tokens) => {
// 批量更新 UI
updateUI(tokens.join(''));
}
});
// 发送消息并处理流
async function sendMessage(sessionId: string, content: string) {
await client.stream(sessionId, content, {
onEvent: (event) => {
// 转换事件为消息格式
const message = EventTransformer.transform(event);
if (event.event_type === 'token') {
// 使用 grouper 优化 token 渲染
tokenGrouper.add(event.data.content);
} else {
// 处理其他事件类型
handleEvent(message);
}
},
onError: (error) => {
console.error('Stream error:', error);
},
onComplete: () => {
tokenGrouper.flush();
console.log('Stream completed');
}
});
}
// 在 Vue/Svelte/原生 JS 中使用
sendMessage('session-123', 'Hello!');Core 模块导出:
ChatClient- SSE 流式客户端ChatManager- 生命周期管理EventTransformer- 事件转换TokenGrouper- Token 聚合优化SessionHistoryService- 历史会话服务
Tailwind CSS 配置
Tailwind v4 (Vite)
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
})/* src/index.css */
@import "tailwindcss";
@import "@so2liu/ma-chat/styles.css";
@source "../node_modules/@so2liu/ma-chat/dist";Tailwind v3
// tailwind.config.js
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx}',
// 添加 chat 包
'./node_modules/@so2liu/ma-chat/dist/**/*.js',
],
// ... 其他配置
};API 参考
ChatProvider Props
interface ChatProviderProps {
backendUrl: string; // 后端 API 地址
authToken?: string; // 认证 token(可选)
fileBaseUrl?: string; // 文件访问基础 URL(可选)
theme?: 'light' | 'dark' | 'system'; // 主题(可选)
children: React.ReactNode;
}ChatWidget Props
interface ChatWidgetProps {
sessionId: string; // 会话 ID(必填)
className?: string; // 额外 CSS 类
isVerboseMode?: boolean; // 详细模式
initialMessage?: string; // 自动发送的初始消息
loadHistory?: boolean; // 是否加载历史(默认 true)
onCardClick?: (card) => void;
onFileClick?: (filePath) => void;
onMessagesChange?: (messages) => void;
}useChat Hook
const {
messages, // Message[] - 所有消息
sendMessage, // (content: string) => Promise<void>
isStreaming, // boolean - 是否正在流式传输
streamContent, // string - 当前流式内容
toolCallStreams, // Record<string, string> - 工具调用流
error, // Error | null - 错误信息
connectionStatus, // 'connected' | 'disconnected' | 'connecting'
} = useChat({
sessionId: string,
backendUrl: string,
onMessage?: (message: Message) => void,
onError?: (error: Error) => void,
});消息类型
interface Message {
id: string;
type: 'user' | 'assistant' | 'event';
content: string;
eventType?: string; // 'token' | 'tool_call' | 'agent_use' | ...
eventData?: unknown;
attachments?: Attachment[];
timestamp: number;
}卡片组件 Props
interface CardComponentProps {
card: {
type: string;
title?: string;
payload?: Record<string, unknown>;
};
sendMessage?: (content: string) => Promise<void>;
sessionId?: string;
}代码块渲染器 Props
interface CodeBlockRendererProps {
code: string; // 代码内容
lang: string; // 语言标识
isStreaming: boolean; // 是否正在流式传输
sessionId: string;
messageId: string;
blockIndex: number;
className?: string;
sendMessage?: (content: string) => Promise<void>;
}消息过滤工具
import {
filterNoiseEvents, // 过滤噪音事件
isUserMessage, // 判断用户消息
isAssistantMessage, // 判断助手消息
isEventMessage, // 判断事件消息
isToolCallMessage, // 判断工具调用消息
NOISE_EVENT_TYPES, // 噪音事件类型集合
} from '@so2liu/ma-chat';
// 使用示例
const cleanMessages = filterNoiseEvents(messages);
const userMessages = messages.filter(isUserMessage);许可证
MIT
