Chat messages → BubbleList adapters

中文:多厂商聊天消息 → BubbleList 适配

This guide explains how to turn messages from common OpenAI, OpenClaw, and Ollama requests/sessions into MessageBubbleData for BubbleList.

Implementation lives in src/Bubble/OpenAIMessageBubble/. It does not depend on vendor npm SDKs; types are defined in-repo.

Table of contents

Design overview

  • Goal: produce MessageBubbleData[] for the bubbleList prop on BubbleList.
  • Two ways in:
    • Hooks: recompute when React state changes (useMemo inside).
    • Pure functions: tests, non-React usage, or custom memoization.
  • Pipeline: OpenClaw and Ollama messages are normalized to an OpenAI-compatible shape, then share the same OpenAI mapper to avoid duplicated logic.
flowchart LR
  subgraph inputs [Input formats]
    OAI[OpenAI messages]
    OC[OpenClaw messages]
    OL[Ollama messages]
  end
  subgraph normalize [Normalize]
    N1[Already OpenAI-shaped]
    N2[normalizeOpenClawMessagesToOpenAI]
    N3[normalizeOllamaMessagesToOpenAI]
  end
  subgraph core [Core]
    MAP[mapOpenAIMessagesToMessageBubbleData]
    MBD[MessageBubbleData array]
  end
  OAI --> N1
  OC --> N2
  OL --> N3
  N1 --> MAP
  N2 --> MAP
  N3 --> MAP
  MAP --> MBD

OpenAI Chat Completions

Typical chat.completions messages: role + content (string or parts), optional tool_calls, tool, function, etc.

Entry pointRole
useOpenAIMessageBubbleData(messages, mapOptions?, mapMessage?)React hook
mapOpenAIMessagesToMessageBubbleData(messages, mapOptions?, mapMessage?)Pure function

Default id: msg.id ?? \openai-msg-${index}`` (content is not hashed, to keep streaming stable).

Notable mapOptions: baseTime / timeStepMs, getMessageId, toolRoleAs (RoleType for tool / function rows), appendToolCallsToContent, preserveRawInExtra (extra.openai.raw), bumpUpdateAtOnLastMessage.

Embedded demo: “OpenAI messages - useOpenAIMessageBubbleData” under Bubble.

OpenClaw session / transcript

Extra fields on top of the OpenAI-like shape:

  • timestamp (ms): optionally applied to createAt / updateAt (useOpenClawTimestamps, default on).
  • toolResult: normalized to an OpenAI tool row; original role is available under extra.openclaw.raw (preserveOpenClawRawInExtra, default on).
Entry pointRole
useOpenClawMessageBubbleDataHook
mapOpenClawMessagesToMessageBubbleDataPure function
normalizeOpenClawMessage(s)ToOpenAIStructure-only conversion

Ollama /api/chat

Aligned with Ollama Chat API ChatMessage: role is system | user | assistant | tool, content is a string; optional images (base64 list), tool_calls, thinking, etc.

thinking and image-count hints are appended as readable placeholders (toggle with appendThinkingToContent, appendImagesPlaceholder). Default id: msg.id ?? \ollama-msg-${index}`; originals under extra.ollama.raw (preserveOllamaRawInExtra`, default on).

Entry pointRole
useOllamaMessageBubbleDataHook
mapOllamaMessagesToMessageBubbleDataPure function
normalizeOllamaMessage(s)ToOpenAIStructure-only conversion

Streaming (SSE) and stable ids

The adapters do not parse SSE; they only consume the messages array you keep in state. On each chunk, update state (for example grow the last assistant content).

Avoid using a hash of content as the message id, or keys will change every token and the list will remount. Prefer:

  • A stable id assigned when the turn starts (server- or client-side), or
  • The default index-based id (index stays fixed while streaming that row).

API cheat sheet

Import from the package entry (also re-exported via ./Bubble):

import {
  useOpenAIMessageBubbleData,
  mapOpenAIMessagesToMessageBubbleData,
  useOpenClawMessageBubbleData,
  mapOpenClawMessagesToMessageBubbleData,
  normalizeOpenClawMessagesToOpenAI,
  useOllamaMessageBubbleData,
  mapOllamaMessagesToMessageBubbleData,
  normalizeOllamaMessagesToOpenAI,
  type OpenAIChatMessage,
  type OpenClawChatMessage,
  type OllamaChatMessage,
} from '@ant-design/agentic-ui';

Per-message overrides: all three paths support mapMessage (signature OpenAIMessagesMapMessage on the OpenAI-shaped step); return an immutable update of draft.

Example with BubbleList

import {
  BubbleList,
  useOpenAIMessageBubbleData,
  type OpenAIChatMessage,
} from '@ant-design/agentic-ui';

const Demo = () => {
  const [messages, setMessages] = useState<OpenAIChatMessage[]>([]);
  const sessionAt = useRef(Date.now()).current;

  const bubbleList = useOpenAIMessageBubbleData(messages, {
    baseTime: sessionAt,
  });

  return (
    <BubbleList
      bubbleList={bubbleList}
      userMeta={{ title: 'User' }}
      assistantMeta={{ title: 'Assistant' }}
    />
  );
};

Changelog

See Bubble in changelog.en-US.md / changelog.zh-CN.md for v2.30.22 and later.