npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@so2liu/ma-chat

v0.2.5

Published

A flexible React chat UI library with multi-level DX support for building AI-powered chat interfaces

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: 快速开始(初学者)

最简单的使用方式,使用 ChatProviderChatWidget

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>
  );
}

关键点:

  • ChatWidgetuseMAClient 都必须在 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