npm package: @so2liu/ma-chat
Description: A flexible React chat UI library with multi-level DX support for building AI-powered chat interfaces
Author: so2liu
Latest version: 0.2.5

# @so2liu/ma-chat

一个灵活的 React 聊天 UI 库，支持多层次的开发者体验（DX），专为构建 AI 驱动的聊天界面设计。

## 特性

- **多层次 DX**：从零配置组件到完全可定制的 headless core
- **流式支持**：实时 SSE 流式传输，带 token 聚合
- **丰富的消息类型**：支持事件、工具调用、规划和自定义卡片
- **可扩展渲染**：卡片注册表和代码块渲染器注册表，支持自定义
- **组件库**：消息列表、事件卡片、模态框等
- **TypeScript 优先**：完整的类型安全和 IntelliSense 支持

## 安装

```bash
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` 内部使用：

```tsx
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`：

```tsx
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 和预置组件构建自定义布局：

```tsx
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: 自定义卡片和代码块（高级）

注册自定义卡片组件和代码块渲染器：

#### 自定义卡片

```tsx
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 返回以下格式时会自动渲染：

~~~markdown
```card+json
{
  "type": "product-card",
  "payload": {
    "name": "智能手表",
    "price": 999,
    "image": "https://example.com/watch.jpg"
  }
}
```
~~~

#### 自定义代码块渲染器

```tsx
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 在任意框架中构建：

```tsx
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)

```ts
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
  plugins: [react(), tailwindcss()],
})
```

```css
/* src/index.css */
@import "tailwindcss";
@import "@so2liu/ma-chat/styles.css";

@source "../node_modules/@so2liu/ma-chat/dist";
```

### Tailwind v3

```js
// tailwind.config.js
module.exports = {
  content: [
    './src/**/*.{js,ts,jsx,tsx}',
    // 添加 chat 包
    './node_modules/@so2liu/ma-chat/dist/**/*.js',
  ],
  // ... 其他配置
};
```

---

## API 参考

### ChatProvider Props

```tsx
interface ChatProviderProps {
  backendUrl: string;        // 后端 API 地址
  authToken?: string;        // 认证 token（可选）
  fileBaseUrl?: string;      // 文件访问基础 URL（可选）
  theme?: 'light' | 'dark' | 'system';  // 主题（可选）
  children: React.ReactNode;
}
```

### ChatWidget Props

```tsx
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

```tsx
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,
});
```

### 消息类型

```tsx
interface Message {
  id: string;
  type: 'user' | 'assistant' | 'event';
  content: string;
  eventType?: string;  // 'token' | 'tool_call' | 'agent_use' | ...
  eventData?: unknown;
  attachments?: Attachment[];
  timestamp: number;
}
```

### 卡片组件 Props

```tsx
interface CardComponentProps {
  card: {
    type: string;
    title?: string;
    payload?: Record<string, unknown>;
  };
  sendMessage?: (content: string) => Promise<void>;
  sessionId?: string;
}
```

### 代码块渲染器 Props

```tsx
interface CodeBlockRendererProps {
  code: string;           // 代码内容
  lang: string;           // 语言标识
  isStreaming: boolean;   // 是否正在流式传输
  sessionId: string;
  messageId: string;
  blockIndex: number;
  className?: string;
  sendMessage?: (content: string) => Promise<void>;
}
```

---

## 消息过滤工具

```tsx
import {
  filterNoiseEvents,    // 过滤噪音事件
  isUserMessage,        // 判断用户消息
  isAssistantMessage,   // 判断助手消息
  isEventMessage,       // 判断事件消息
  isToolCallMessage,    // 判断工具调用消息
  NOISE_EVENT_TYPES,    // 噪音事件类型集合
} from '@so2liu/ma-chat';

// 使用示例
const cleanMessages = filterNoiseEvents(messages);
const userMessages = messages.filter(isUserMessage);
```

---

## 许可证

MIT