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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xinghunm/ai-chat

v1.4.2

Published

AI chat React component library

Downloads

956

Readme

@xinghunm/ai-chat

提供完整 AI 对话 UI 的 React 组件库,支持会话管理、流式响应、Agent 模式和图片附件。

安装

npm install @xinghunm/ai-chat
# 或
pnpm add @xinghunm/ai-chat

Peer Dependencies

需在项目中单独安装:

npm install @emotion/react @emotion/styled @xinghunm/compass-ui axios react react-dom react-markdown rehype-katex remark-gfm remark-math zustand

快速开始

推荐通过 transport 接入,这样 ai-chat 只负责通用 UI,后端协议由接入层决定:

import { AiChat, createDefaultChatTransport } from '@xinghunm/ai-chat'

const transport = createDefaultChatTransport({
  apiBaseUrl: '/ai-api',
  authToken: 'Bearer your-token-here',
})

export const App = () => <AiChat transport={transport} />

显示会话列表侧边栏:

<AiChat transport={transport} showConversationList />

接入历史会话列表时,ai-chat 不请求后端 API。宿主应用负责分页获取会话列表,并把已加载的数据传给组件;列表滚动到底部时,组件会触发 onLoadMoreSessions

import { useState } from 'react'
import type { ChatMessage, ChatSession } from '@xinghunm/ai-chat'

const [sessions, setSessions] = useState<ChatSession[]>([])
const [isLoading, setIsLoading] = useState(false)
const [cursor, setCursor] = useState<string | null>(null)

const loadMoreSessions = async () => {
  if (isLoading) return

  setIsLoading(true)
  const response = await fetch(`/chat/sessions?cursor=${cursor ?? ''}`)
  const data = (await response.json()) as {
    sessions: ChatSession[]
    nextCursor: string | null
  }

  setSessions((current) => [...current, ...data.sessions])
  setCursor(data.nextCursor)
  setIsLoading(false)
}

const loadSessionMessages = async (session: ChatSession): Promise<ChatMessage[]> => {
  const response = await fetch(`/chat/sessions/${session.sessionId}/messages`)
  const data = (await response.json()) as { messages: ChatMessage[] }
  return data.messages
}

export const App = () => (
  <AiChat
    transport={transport}
    showConversationList
    historySessionList={{
      sessions,
      isLoading,
      hasMore: cursor !== null,
      error: null,
    }}
    onLoadMoreSessions={loadMoreSessions}
    onSelectHistorySession={loadSessionMessages}
  />
)

如果你还在使用内置默认协议,也可以继续传 apiBaseUrlauthTokenAiChatProvider 会在内部自动创建默认 transport,属于兼容模式。

如果只是路径不同,也不需要自己重写整个 transport,可以只覆盖默认 adapter 的 endpoints:

const transport = createDefaultChatTransport({
  apiBaseUrl: '/ai-api',
  authToken: 'Bearer your-token-here',
  endpoints: {
    models: '/catalog/models',
    completions: '/chat/run',
    terminate: '/chat/stop',
  },
})

如果后端协议大体一致,只是模型列表或 /chat/completions 请求体需要调整,也可以继续复用默认 transport,只覆盖局部扩展点:

const transport = createDefaultChatTransport({
  apiBaseUrl: '/ai-api',
  authToken: 'Bearer your-token-here',
  resolveModels: async () => ({
    data: [{ id: 'deepseek-chat', object: 'model' }],
  }),
  buildRequestBody: ({ model, mode, content }) => ({
    model: 'deepseek-chat',
    base_url: 'https://api.deepseek.com/v1',
    api_key: 'sk-xxxxx',
    mode,
    stream: true,
    messages: [{ role: 'user', content }],
  }),
})

默认 transport 在检测到图片附件时,会自动把用户输入组装成多模态 messages[].content 数组,并将图片文件序列化为 Data URL 后发送;如果你自定义 buildRequestBody,也会同时收到 attachments 参数,可按后端协议自行处理。

完整用法

如需最大灵活性,可在 AiChatProvider 内手动组合子组件:

import { AiChatProvider, ChatConversationList, ChatThread, ChatComposer } from '@xinghunm/ai-chat'
import type { ChatTransport } from '@xinghunm/ai-chat'

const transport: ChatTransport = {
  async getModels() {
    return {
      data: [{ id: 'my-model', object: 'model' }],
    }
  },
  async startStream({
    sessionId,
    model,
    mode,
    content,
    attachments,
    onSessionId,
    onUpdate,
    onDone,
    onError,
    signal,
  }) {
    try {
      const response = await fetch('/custom-chat/stream', {
        method: 'POST',
        signal,
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ sessionId, model, mode, content, attachments }),
      })
      const data = await response.json()
      onSessionId?.(data.sessionId)
      onUpdate({ content: data.answer })
      onDone?.()
    } catch (error) {
      onError?.(error instanceof Error ? error : new Error('Unknown stream error'))
    }
  },
  async terminateStream(sessionId) {
    await fetch('/custom-chat/terminate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ sessionId }),
    })
    return { terminated: true }
  },
}

export const CustomChat = () => (
  <AiChatProvider
    transport={transport}
    defaultMode="agent"
    labels={{
      sendButton: '发送',
      placeholder: '输入你的问题…',
      newChat: '新建对话',
    }}
  >
    <div style={{ display: 'flex', height: '100vh' }}>
      <ChatConversationList />
      <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
        <ChatThread />
        <ChatComposer />
      </div>
    </div>
  </AiChatProvider>
)

Props API

AiChatProps

一体化 AiChat 组件的 Props,继承全部 AiChatProviderProps(不含 children)。

| 属性 | 类型 | 默认值 | 说明 | | ------------------------ | ---------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------- | | transport | ChatTransport | — | 推荐。由接入方提供的传输适配器。 | | apiBaseUrl | string | — | 兼容模式。创建默认内置 transport。 | | authToken | string | — | 兼容模式下默认 transport 使用的鉴权头。 | | defaultMode | ChatAgentMode | "agent" | 新会话的初始 Agent 模式。 | | labels | AiChatLabels | — | 可选的 UI 文案覆盖。 | | showConversationList | boolean | false | 为 true 时渲染会话列表侧边栏。 | | historySessionList | ChatHistorySessionListState | — | 受控历史会话列表状态。宿主应用负责请求后端并传入已加载会话。 | | onLoadMoreSessions | () => void \| Promise<void> | — | 会话列表滚动到底部且 hasMoretrue 时触发,由宿主应用加载下一页。 | | onSelectHistorySession | (session) => ChatMessage[] \| void \| Promise<ChatMessage[] \| void> | — | 点击未加载过的历史会话时触发。返回消息数组时组件会写入内部 store;返回 void 时宿主自行控制。 | | messageRenderOrder | ChatMessageRenderOrder | "blocks-first" | 混合纯文本与结构化 block 时的渲染顺序。默认结构化卡片在前;设为 "timeline" 时按时间线优先保留文本在前。 | | enableImageAttachments | boolean | true | 为 false 时隐藏上传按钮、禁用粘贴图片,并使程序化调用 pickImages/pasteImages 变为 no-op。 |

AiChatProviderProps

AiChatProvider 上下文提供者组件的 Props。

AiChatProvider 有两种接入方式,二选一:

| 属性 | 类型 | 必填 | 说明 | | ------------------------ | ---------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------ | | transport | ChatTransport | 是 | 推荐。完全自定义的传输层。 | | defaultMode | ChatAgentMode | 否 | 新会话的初始 Agent 模式。 | | labels | AiChatLabels | 否 | 可选的 UI 文案覆盖。 | | renderMessageBlock | ChatMessageBlockRenderer | 否 | 自定义消息 block 渲染器,用于承接 type: "custom" 等扩展。 | | messageRenderOrder | ChatMessageRenderOrder | 否 | 混合纯文本与结构化 block 时的渲染顺序。默认 "blocks-first",设为 "timeline" 可按时间线优先渲染文本片段。 | | historySessionList | ChatHistorySessionListState | 否 | 受控历史会话列表状态。 | | onLoadMoreSessions | () => void \| Promise<void> | 否 | 会话列表滚动到底部且仍有下一页时触发。 | | onSelectHistorySession | (session) => ChatMessage[] \| void \| Promise<ChatMessage[] \| void> | 否 | 点击未加载过的历史会话时触发。 | | enableImageAttachments | boolean | 否 | 为 false 时隐藏上传按钮、禁用粘贴图片,并使程序化调用 pickImages/pasteImages 变为 no-op。默认 true。 | | children | ReactNode | 是 | Provider 内部渲染的子元素。 |

兼容模式:

| 属性 | 类型 | 必填 | 说明 | | ------------------------ | ---------------------------------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------ | | apiBaseUrl | string | 是 | 默认 adapter 的基础 URL。 | | authToken | string | 是 | 默认 adapter 使用的 Authorization 请求头值。 | | transformStreamPacket | TransformChatStreamPacket | 否 | 默认 adapter 的流式包归一化扩展点。 | | defaultMode | ChatAgentMode | 否 | 新会话的初始 Agent 模式。 | | labels | AiChatLabels | 否 | 可选的 UI 文案覆盖。 | | renderMessageBlock | ChatMessageBlockRenderer | 否 | 自定义消息 block 渲染器,用于承接 type: "custom" 等扩展。 | | messageRenderOrder | ChatMessageRenderOrder | 否 | 混合纯文本与结构化 block 时的渲染顺序。默认 "blocks-first",设为 "timeline" 可按时间线优先渲染文本片段。 | | historySessionList | ChatHistorySessionListState | 否 | 受控历史会话列表状态。 | | onLoadMoreSessions | () => void \| Promise<void> | 否 | 会话列表滚动到底部且仍有下一页时触发。 | | onSelectHistorySession | (session) => ChatMessage[] \| void \| Promise<ChatMessage[] \| void> | 否 | 点击未加载过的历史会话时触发。 | | enableImageAttachments | boolean | 否 | 为 false 时隐藏上传按钮、禁用粘贴图片,并使程序化调用 pickImages/pasteImages 变为 no-op。默认 true。 | | children | ReactNode | 是 | Provider 内部渲染的子元素。 |

ChatTransport

ChatTransport 是通用化后的核心扩展点:

| 方法 | 说明 | | ------------------- | -------------------------------------------------------- | | getModels() | 返回模型列表,用于填充模型选择器。 | | startStream() | 发送用户消息并通过 onUpdate 推送归一化后的流式 patch。 | | terminateStream() | 请求终止当前会话的流式响应。 |

库内同时导出了 createDefaultChatTransport(),用于快速复用当前 /models/chat/completions/chat/terminate 协议。

createDefaultChatTransport() 同时支持 endpoints 覆盖:

| 键 | 默认值 | 说明 | | ------------- | --------------------- | ---------------------- | | models | "/models" | 模型列表接口路径。 | | completions | "/chat/completions" | 流式聊天接口路径。 | | terminate | "/chat/terminate" | 停止流式响应接口路径。 |

此外还支持两个轻量扩展点:

| 键 | 类型 | 说明 | | ------------------ | ----------------------------------- | ------------------------------------------------------------------------------ | | resolveModels | () => Promise<ChatModelsResponse> | 覆盖默认的 /models 请求,直接返回模型列表。 | | buildRequestBody | (args) => unknown | 覆盖默认的 /chat/completions 请求体构造逻辑;args 同时包含 attachments。 |

AiChatLabels

所有字段均为可选,未指定的字段回退到 DEFAULT_AI_CHAT_LABELS 中的英文默认值。

| 键 | 默认值 | 说明 | | ----------------------- | ------------------------------ | ------------------------------ | | sendButton | "Send" | 发送按钮文案。 | | stopButton | "Stop" | 停止/中止按钮文案。 | | placeholder | "Ask something..." | 输入框占位文本。 | | modeLabelAsk | "Ask" | Ask 模式标签。 | | modeLabelPlan | "Plan" | Plan 模式标签。 | | modeLabelAgent | "Agent" | Agent 模式标签。 | | newChat | "New Chat" | 新建对话按钮文案。 | | emptyStateTitle | "How can I help you?" | 空消息状态的主标题。 | | emptyStateSubtitle | "Start a conversation" | 空消息状态的副标题。 | | attachmentLimitNotice | "Images exceeded the limit…" | 达到附件数量上限时的提示文案。 |

Store

在高级场景下(如读取流式状态、以编程方式切换会话),可在 AiChatProvider 的子孙组件内通过内置 hooks 访问底层 Zustand store:

import { useChatStore, useChatContext } from '@xinghunm/ai-chat'

// 选取 store 的某个切片(仅在该切片变化时触发重渲染)
const activeSessionId = useChatStore((s) => s.activeSessionId)

// 获取完整上下文,包括 transport 和合并后的 labels
const { labels, transport } = useChatContext()

两个 hooks 在 AiChatProvider 外部调用时均会抛出异常。

扩展自定义 Block

ai-chat 现在支持把“通用聊天壳子”和“业务工作流卡片”解耦。推荐做法是:

  1. transformStreamPacket 把后端的自定义 packet 转成 blocks
  2. renderMessageBlock 渲染 type: "custom" 的 block
import type { ChatMessageBlockRendererProps, TransformChatStreamPacket } from '@xinghunm/ai-chat'

const transformStreamPacket: TransformChatStreamPacket = ({ packet, defaultUpdate }) => {
  if (
    packet.type === 'message_complete' &&
    typeof packet.data === 'object' &&
    packet.data !== null &&
    'widget' in packet.data
  ) {
    return {
      ...defaultUpdate,
      blocks: [
        { type: 'custom', kind: 'widget', data: (packet.data as { widget: unknown }).widget },
      ],
    }
  }

  return defaultUpdate
}

const renderMessageBlock = ({ block }: ChatMessageBlockRendererProps) => {
  if (block.type !== 'custom' || block.kind !== 'widget') {
    return null
  }

  return <pre>{JSON.stringify(block.data, null, 2)}</pre>
}

这样业务卡片可以放在接入层或扩展包里,基础包继续只负责通用聊天体验。

控制消息渲染顺序

默认情况下,ai-chat 采用 blocks-first 语义:一条消息同时包含 content 和结构化 blocks 时,会先渲染卡片类 block,再渲染正文。这适合参数卡、确认卡、结果卡等“先看结构化信息,再看解释文本”的场景。

如果你的接入层会在流式文本中途插入审批卡片、工作流卡片或其他结构化 block,可以把 messageRenderOrder 设为 "timeline"。这样在消息同时存在纯文本和非 markdown block 时,组件会优先保留文本在前,再接上 block,更接近真实到达顺序。

从当前版本开始,timeline 模式还会自动记录结构化 block 第一次出现时对应的文本位置。也就是说:

  • 当正文已经流出一部分时,后续才收到审批卡或其他自定义 block,ai-chat 会把这张卡片锚定在它首次出现的位置。
  • 卡片出现之后继续流出的新文本,会自动渲染到卡片后面。
  • 业务侧不需要再为了“卡片停留在触发点”手动把前文转成 markdown block

这尤其适合下面这种真实流式场景:

  1. assistant 先输出一段解释文本
  2. 中途触发 approval_required 或其他自定义卡片
  3. assistant 在卡片之后继续输出补充说明

在这种情况下,timeline 会自动把消息排成 “前文 -> 卡片 -> 后文”。

import { AiChatProvider, ChatThread, ChatComposer } from '@xinghunm/ai-chat'

export const CustomChat = () => (
  <AiChatProvider
    transport={transport}
    messageRenderOrder="timeline"
    renderMessageBlock={({ block }) => {
      if (block.type !== 'custom' || block.kind !== 'tool-approval') {
        return null
      }

      return <ToolApprovalCard data={block.data} />
    }}
  >
    <ChatThread />
    <ChatComposer />
  </AiChatProvider>
)

选择建议:

  • 继续使用默认值 "blocks-first":适合静态消息排版,结构化信息应始终优先展示。
  • 使用 "timeline":适合流式响应中途插卡,希望卡片停留在触发事件位置,且不想在业务侧维护额外文本冻结逻辑的场景。