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

@kne/react-bot

v0.1.4

Published

一个用于 AI 对话与 IM 消息展示的 React 组件库,提供 ReactBot、MessageList、MessageInput、VoiceInput,支持 SSE 流式/Ajax 双模式、Markdown 渲染、语音输入与可扩展工具栏

Readme

react-bot

描述

一个用于 AI 对话与 IM 消息展示的 React 组件库,提供 ReactBot、MessageList、MessageInput、VoiceInput,支持 SSE 流式/Ajax 双模式、Markdown 渲染、语音输入与可扩展工具栏

安装

npm i --save @kne/react-bot

概述

用于 AI 聊天对话与 IM 消息展示,提供 MessageList、MessageInput、ReactBot:支持左右气泡、可扩展工具栏与语音输入、axios-fetch SSE/Ajax 交互、Markdown 渲染,以及可选的左侧实时内容面板(默认关闭)。可嵌入 @kne/system-layoutaiDialog 作为系统级 AI 助手。

示例

示例样式

@use '~@kne/responsive-utils/scss' as resp;

/* 勿覆盖 example-driver-preview 的 padding/min-height:手机会预览依赖其布局 */

.example-driver-preview {
  background: #f6f7f9;
}

/**
 * 示例聊天外壳:桌面固定高度;手机预览(container)下高度占满设备屏。
 */
.react-bot-example-shell {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  width: 100%;
  max-width: 800px;
  height: 560px;
  margin: 0 auto;
  min-height: 0;
}

/** 桌面随内容增高,仅移动端占满屏(MessageList / MessageInput 等) */
.react-bot-example-shell-fluid {
  box-sizing: border-box;
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
}

@include resp.mobile-container {
  .react-bot-example-shell,
  .react-bot-example-shell-fluid {
    max-width: none;
    width: 100%;
    margin: 0;
    height: var(--kne-viewport-height, 100dvh);
    min-height: var(--kne-viewport-height, 100dvh);
  }
}

/**
 * system-layout aiDialog 内容区:纵向 flex,让 ReactBot 能 flex:1 撑满。
 * 滚动交给 ReactBot 内部 .message-list-outer,不要在这里 overflow:hidden。
 */
.page-window-content,
.ai-dialog-window-content {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

/* SystemLayout.aiDialog 内嵌 ReactBot:撑满内容区、去掉默认白底 */
.react-bot-system-layout-dialog {
  --message-bubble-bg-left: #fff;
  flex: 1 1 0% !important;
  min-height: 0 !important;
  width: 100%;
  background: transparent !important;
}

示例代码

  • MessageList
  • 消息列表左右分布,展示不同 role 的头像、姓名、气泡与底部操作
  • _ReactBot(@kne/current-lib_react-bot)[import * as _ReactBot from "@kne/react-bot"],(@kne/current-lib_react-bot/dist/index.css),antd(antd)
const { MessageList } = _ReactBot;
const { Flex, Button, Space, App } = antd;

const messages = [
  {
    id: '1',
    role: 'assistant',
    name: '客服小助手',
    content: '你好,我是客服助手,有什么可以帮你?'
  },
  {
    id: '2',
    role: 'user',
    name: '张三',
    content: '想咨询一下订单物流进度'
  },
  {
    id: '3',
    role: 'assistant',
    name: '客服小助手',
    content: '好的,请提供一下订单号,我帮你查询。\n\n也可以查看 [物流说明](https://example.com)。'
  },
  {
    id: '4',
    role: 'user',
    name: '张三',
    content: '订单号 ORD-20260731-8899',
    status: 'sent'
  }
];

const BaseExample = () => {
  return (
    <App>
      <Flex vertical gap={16} className="react-bot-example-shell-fluid" style={{ background: '#fff', borderRadius: 8 }}>
        <MessageList
          title="客服会话"
          messages={messages}
          roles={{
            user: { placement: 'right', name: '我' },
            assistant: { placement: 'left', name: '客服' }
          }}
          renderFooter={(message, roleConfig) => {
            if (message.role !== 'assistant') return null;
            return (
              <Space size={4}>
                <Button type="link" size="small">
                  复制
                </Button>
                <Button type="link" size="small">
                  重新生成
                </Button>
              </Space>
            );
          }}
        />
      </Flex>
    </App>
  );
};

render(<BaseExample />);
  • MessageInput
  • 输入框:工具栏扩展、文本发送与语音输入入口
  • _ReactBot(@kne/current-lib_react-bot)[import * as _ReactBot from "@kne/react-bot"],(@kne/current-lib_react-bot/dist/index.css),antd(antd),icons(@ant-design/icons)
const { MessageInput } = _ReactBot;
const { Flex, App, message: antdMessage, Radio, Typography } = antd;
const { PictureOutlined, PaperClipOutlined, SmileOutlined, PhoneOutlined } = icons;

const BaseExample = () => {
  const [value, setValue] = React.useState('');
  const [borderType, setBorderType] = React.useState('aurora');
  return (
    <App>
      <Flex vertical gap={12} className="react-bot-example-shell-fluid">
        <Flex align="center" gap={8}>
          <Typography.Text>边框样式:</Typography.Text>
          <Radio.Group
            optionType="button"
            value={borderType}
            onChange={e => setBorderType(e.target.value)}
            options={[
              { label: '炫彩边框', value: 'aurora' },
              { label: '普通边框', value: 'default' }
            ]}
          />
        </Flex>
        <MessageInput
          value={value}
          onChange={setValue}
          borderType={borderType}
          showVoice={false}
          toolbarItems={[
            {
              key: 'emoji',
              icon: <SmileOutlined />,
              title: '表情',
              onClick: () => antdMessage.info('扩展:打开表情面板')
            },
            {
              key: 'image',
              icon: <PictureOutlined />,
              title: '图片',
              onClick: () => antdMessage.info('扩展:选择图片')
            },
            {
              key: 'file',
              icon: <PaperClipOutlined />,
              title: '附件',
              onClick: () => antdMessage.info('扩展:上传附件')
            },
            {
              key: 'call',
              icon: <PhoneOutlined />,
              title: '通话',
              onClick: () => antdMessage.info('扩展:发起语音/视频通话')
            }
          ]}
          onSend={text => {
            antdMessage.success(&#96;已发送:${text}&#96;);
            setValue('');
          }}
        />
      </Flex>
    </App>
  );
};

render(<BaseExample />);
  • MessageInput Voice
  • 语音输入 Mock 演示:注入 mock speechTextRealTime,流式打出转写文案,可发送/取消
  • _ReactBot(@kne/current-lib_react-bot)[import * as _ReactBot from "@kne/react-bot"],(@kne/current-lib_react-bot/dist/index.css),antd(antd)
const { MessageInput, VoiceInput } = _ReactBot;
const { Flex, App, Alert, Typography, Button, Tag, message: antdMessage } = antd;

/**
 * 模拟 getSpeechToken:真实项目请换成后端签发的阿里/讯飞凭证。
 */
const mockGetSpeechToken = async () => {
  await new Promise(resolve => setTimeout(resolve, 200));
  return {
    appKey: 'demo-app-key',
    token: 'demo-speech-token',
    expire: Date.now() + 60 * 60 * 1000
  };
};

/**
 * Mock 实时语音转写:不申请麦克风、不连真实语音服务,
 * 按字符流式输出演示文案,便于文档页交互预览。
 */
const createMockSpeechTextRealTime =
  (fullText = '你好,这是一段模拟语音转写的演示文案,可以直接发送。') =>
  ({ getToken, onChange, onError }) => {
    let timer = null;
    let index = 0;
    let stopped = false;

    return Promise.resolve({
      start: async () => {
        try {
          const token = await getToken();
          if (!token || stopped) {
            return;
          }
          timer = setInterval(() => {
            if (stopped) {
              clearInterval(timer);
              return;
            }
            index = Math.min(index + 2, fullText.length);
            onChange && onChange({ message: fullText.slice(0, index) });
            if (index >= fullText.length) {
              clearInterval(timer);
              timer = null;
            }
          }, 60);
        } catch (e) {
          onError && onError(e?.message || 'Mock speech failed');
        }
      },
      stop: () => {
        stopped = true;
        if (timer) {
          clearInterval(timer);
          timer = null;
        }
      }
    });
  };

const mockSpeechTextRealTime = createMockSpeechTextRealTime();

const BaseExample = () => {
  const [value, setValue] = React.useState('');
  const [showStandalone, setShowStandalone] = React.useState(false);
  const [sentList, setSentList] = React.useState([]);

  return (
    <App>
      <Flex vertical gap={16} className="react-bot-example-shell-fluid">
        <Alert
          type="info"
          showIcon
          message={
            <Flex align="center" gap={8}>
              <span>语音输入(Mock 演示)</span>
              <Tag color="processing">Mock</Tag>
            </Flex>
          }
          description="本示例通过 speechTextRealTime 注入 Mock 引擎,无需麦克风与真实 Token。点击麦克风后会流式打出演示文案,可点「发送」或「取消」。生产环境去掉 Mock,改用 @kne/speech-text 默认实现即可。"
        />

        <Typography.Title level={5} style={{ margin: 0 }}>
          MessageInput 内置语音按钮
        </Typography.Title>
        <MessageInput
          value={value}
          onChange={setValue}
          showVoice
          getSpeechToken={mockGetSpeechToken}
          speechTextRealTime={mockSpeechTextRealTime}
          placeholder="点右侧麦克风开始 Mock 语音输入"
          onSend={text => {
            setSentList(list => [...list, { type: 'input', text, time: Date.now() }]);
            antdMessage.success(&#96;已发送:${text}&#96;);
            setValue('');
          }}
        />

        <Typography.Title level={5} style={{ margin: 0 }}>
          独立 VoiceInput
        </Typography.Title>
        {!showStandalone ? (
          <Button type="dashed" onClick={() => setShowStandalone(true)}>
            打开独立语音面板(Mock)
          </Button>
        ) : (
          <div style={{ border: '1px solid #eee', borderRadius: 12, padding: 12, background: '#fff' }}>
            <VoiceInput
              getSpeechToken={mockGetSpeechToken}
              speechTextRealTime={createMockSpeechTextRealTime('独立 VoiceInput 的模拟转写结果,点击发送试试。')}
              onComplete={text => {
                setShowStandalone(false);
                if (text) {
                  setSentList(list => [...list, { type: 'voice', text, time: Date.now() }]);
                  antdMessage.success(&#96;语音发送:${text}&#96;);
                } else {
                  antdMessage.info('已取消语音输入');
                }
              }}
              onCancel={() => {
                setShowStandalone(false);
                antdMessage.info('已取消语音输入');
              }}
            />
          </div>
        )}

        {sentList.length > 0 && (
          <Flex vertical gap={8}>
            <Typography.Text type="secondary">已发送记录</Typography.Text>
            {sentList.map(item => (
              <Typography.Paragraph key={item.time} style={{ margin: 0 }}>
                [{item.type === 'voice' ? '语音' : '输入'}] {item.text}
              </Typography.Paragraph>
            ))}
          </Flex>
        )}
      </Flex>
    </App>
  );
};

render(<BaseExample />);
  • ReactBot SSE
  • SSE 流式对接 AI,支持 Markdown 渲染与 Mock 语音输入
  • _ReactBot(@kne/current-lib_react-bot)[import * as _ReactBot from "@kne/react-bot"],(@kne/current-lib_react-bot/dist/index.css),_AxiosFetch(@kne/axios-fetch)[import * as _AxiosFetch from "@kne/axios-fetch"],antd(antd)
const { ReactBot } = _ReactBot;
const { default: createAjax } = _AxiosFetch;
const { App, Flex } = antd;

const ajax = createAjax({
  baseURL: 'https://example.com',
  errorHandler: () => {}
});

const mockGetSpeechToken = async () => {
  await new Promise(resolve => setTimeout(resolve, 200));
  return {
    appKey: 'demo-app-key',
    token: 'demo-speech-token',
    expire: Date.now() + 60 * 60 * 1000
  };
};

/** Mock 语音转写:无需麦克风,流式输出演示文案 */
const mockSpeechTextRealTime = ({ getToken, onChange, onError }) => {
  const fullText = '你好,这是 Mock 语音输入,可以直接发送给 AI。';
  let timer = null;
  let index = 0;
  let stopped = false;
  return Promise.resolve({
    start: async () => {
      try {
        const token = await getToken();
        if (!token || stopped) return;
        timer = setInterval(() => {
          if (stopped) {
            clearInterval(timer);
            return;
          }
          index = Math.min(index + 2, fullText.length);
          onChange && onChange({ message: fullText.slice(0, index) });
          if (index >= fullText.length) {
            clearInterval(timer);
            timer = null;
          }
        }, 60);
      } catch (e) {
        onError && onError(e?.message || 'Mock speech failed');
      }
    },
    stop: () => {
      stopped = true;
      if (timer) {
        clearInterval(timer);
        timer = null;
      }
    }
  });
};

const mockReplies = {
  你好: '你好!我是 ReactBot 助手,可以帮你解答问题。',
  markdown:
    '这是一段 **Markdown** 回复:\n\n- 支持列表\n- 支持 &#96;代码&#96;\n\n&#96;&#96;&#96;js\nconsole.log("hello");\n&#96;&#96;&#96;',
  图片: '这是一张示例图片:\n\n![demo](https://picsum.photos/seed/react-bot/640/360)'
};
const defaultReply =
  '收到!这是基于 axios-fetch SSE 的流式回复示例。你可以试试发送「你好」「markdown」或「图片」,也可以点麦克风用 Mock 语音输入。';

function createMockEventSource(fullText) {
  let index = 0;
  let timer = null;
  const listeners = {};
  const messageId = 'assistant-' + Date.now();

  const MockES = function () {
    this.readyState = 0;
    this.onopen = null;
    this.onmessage = null;
    this.onerror = null;

    setTimeout(() => {
      this.readyState = 1;
      if (typeof this.onopen === 'function') this.onopen({ type: 'open' });
      timer = setInterval(() => {
        if (this.readyState !== 1) return;
        if (index >= fullText.length) {
          clearInterval(timer);
          timer = null;
          this._dispatch('message', JSON.stringify({ id: messageId, content: '', done: true }));
          this.readyState = 2;
          return;
        }
        const chunkSize = Math.floor(Math.random() * 3) + 1;
        const chunk = fullText.slice(index, index + chunkSize);
        index += chunkSize;
        this._dispatch('message', JSON.stringify({ id: messageId, content: chunk, done: false }));
      }, 30);
    }, 200);
  };

  MockES.CONNECTING = 0;
  MockES.OPEN = 1;
  MockES.CLOSED = 2;
  MockES.prototype.addEventListener = function (type, handler) {
    if (!listeners[type]) listeners[type] = [];
    listeners[type].push(handler);
  };
  MockES.prototype._dispatch = function (type, data) {
    const event = { type, data };
    if (type === 'message' && typeof this.onmessage === 'function') this.onmessage(event);
    (listeners[type] || []).forEach(handler => handler(event));
  };
  MockES.prototype.close = function () {
    this.readyState = 2;
    if (timer) clearInterval(timer);
  };
  return MockES;
}

function getMockReply(input) {
  const key = Object.keys(mockReplies).find(k => input.includes(k));
  return key ? mockReplies[key] : defaultReply;
}

const BaseExample = () => {
  return (
    <App>
      <Flex className="react-bot-example-shell">
        <ReactBot
          title="AI 助手"
          ajax={ajax}
          requestMode="sse"
          api={{ url: '/ai/chat' }}
          getRequest={({ content }) => ({
            url: '/ai/chat',
            params: { prompt: content },
            EventSource: createMockEventSource(getMockReply(content))
          })}
          messageInputProps={{
            showVoice: true,
            getSpeechToken: mockGetSpeechToken,
            speechTextRealTime: mockSpeechTextRealTime,
            placeholder: '输入消息,或点麦克风使用 Mock 语音'
          }}
          defaultMessages={[
            {
              id: 'welcome',
              role: 'assistant',
              content: '你好,我是 ReactBot。可以打字发送,也可以点麦克风试用 Mock 语音输入。'
            }
          ]}
        />
      </Flex>
    </App>
  );
};

render(<BaseExample />);
  • ReactBot Side Panel
  • 左侧面板按发言轮播:产品图 → 产品视频 → 超长单卡 → 长内容滚动 → 纯文本保持上一条 → ProductCard / SpecCard / antd Card
  • _ReactBot(@kne/current-lib_react-bot)[import * as _ReactBot from "@kne/react-bot"],(@kne/current-lib_react-bot/dist/index.css),_AxiosFetch(@kne/axios-fetch)[import * as _AxiosFetch from "@kne/axios-fetch"],antd(antd)
const { ReactBot } = _ReactBot;
const { default: createAjax } = _AxiosFetch;
const { App, Flex, Card, Button, Tag, Descriptions, Space, Typography } = antd;
const { Text, Paragraph } = Typography;

const ajax = createAjax({
  baseURL: 'https://example.com',
  errorHandler: () => {}
});

const mockGetSpeechToken = async () => {
  await new Promise(resolve => setTimeout(resolve, 200));
  return {
    appKey: 'demo-app-key',
    token: 'demo-speech-token',
    expire: Date.now() + 60 * 60 * 1000
  };
};

const mockSpeechTextRealTime = ({ getToken, onChange, onError }) => {
  const fullText = '下一张';
  let timer = null;
  let index = 0;
  let stopped = false;
  return Promise.resolve({
    start: async () => {
      try {
        const token = await getToken();
        if (!token || stopped) return;
        timer = setInterval(() => {
          if (stopped) {
            clearInterval(timer);
            return;
          }
          index = Math.min(index + 2, fullText.length);
          onChange && onChange({ message: fullText.slice(0, index) });
          if (index >= fullText.length) {
            clearInterval(timer);
            timer = null;
          }
        }, 60);
      } catch (e) {
        onError && onError(e?.message || 'Mock speech failed');
      }
    },
    stop: () => {
      stopped = true;
      if (timer) {
        clearInterval(timer);
        timer = null;
      }
    }
  });
};

function createMockEventSource(fullText) {
  let index = 0;
  let timer = null;
  const listeners = {};
  const messageId = 'assistant-side-' + Date.now();

  const MockES = function () {
    this.readyState = 0;
    this.onopen = null;
    this.onmessage = null;
    this.onerror = null;
    setTimeout(() => {
      this.readyState = 1;
      if (typeof this.onopen === 'function') this.onopen({ type: 'open' });
      timer = setInterval(() => {
        if (this.readyState !== 1) return;
        if (index >= fullText.length) {
          clearInterval(timer);
          this._dispatch('message', JSON.stringify({ id: messageId, content: '', done: true }));
          this.readyState = 2;
          return;
        }
        const chunkSize = 2;
        const chunk = fullText.slice(index, index + chunkSize);
        index += chunkSize;
        this._dispatch('message', JSON.stringify({ id: messageId, content: chunk, done: false }));
      }, 20);
    }, 150);
  };
  MockES.CONNECTING = 0;
  MockES.OPEN = 1;
  MockES.CLOSED = 2;
  MockES.prototype.addEventListener = function (type, handler) {
    if (!listeners[type]) listeners[type] = [];
    listeners[type].push(handler);
  };
  MockES.prototype._dispatch = function (type, data) {
    const event = { type, data };
    if (type === 'message' && typeof this.onmessage === 'function') this.onmessage(event);
    (listeners[type] || []).forEach(handler => handler(event));
  };
  MockES.prototype.close = function () {
    this.readyState = 2;
    if (timer) clearInterval(timer);
  };
  return MockES;
}

/** 产品卡片:展示标题、标签与操作按钮 */
const ProductCard = ({ title, cover, tags, price, description, onAction }) => {
  const tagList = (Array.isArray(tags) ? tags : []).filter(Boolean);
  return (
    <Card
      size="small"
      cover={cover ? <img alt={title} src={cover} style={{ height: 160, objectFit: 'cover' }} /> : null}
      actions={[
        <Button key="detail" type="link" onClick={onAction}>
          查看详情
        </Button>
      ]}>
      <Card.Meta
        title={
          <Space>
            <span>{title}</span>
            {price != null && <Text type="danger">{price}</Text>}
          </Space>
        }
        description={
          <Space direction="vertical" size={6} style={{ width: '100%' }}>
            <div>{description}</div>
            {tagList.length > 0 && (
              <Space size={4} wrap>
                {tagList.map(tag => (
                  <Tag key={tag} color="blue">
                    {tag}
                  </Tag>
                ))}
              </Space>
            )}
          </Space>
        }
      />
    </Card>
  );
};

/** 规格说明卡片 */
const SpecCard = ({ title, items }) => {
  const itemList = (Array.isArray(items) ? items : []).filter(item => item && item.label != null);
  return (
    <Card size="small" title={title}>
      <Descriptions column={1} size="small">
        {itemList.map(item => (
          <Descriptions.Item key={item.label} label={item.label}>
            {item.value}
          </Descriptions.Item>
        ))}
      </Descriptions>
    </Card>
  );
};

/** 单张超长内容卡片:用于验收侧栏滚动与进出场 */
const LongArticleCard = ({ title, subtitle, paragraphs }) => {
  const list = (Array.isArray(paragraphs) ? paragraphs : []).filter(Boolean);
  return (
    <Card size="small" title={title} extra={subtitle ? <Text type="secondary">{subtitle}</Text> : null}>
      <Space direction="vertical" size={12} style={{ width: '100%' }}>
        {list.map((text, index) => (
          <Paragraph key={index} style={{ marginBottom: 0 }}>
            {text}
          </Paragraph>
        ))}
      </Space>
    </Card>
  );
};

/** 每次用户发言只回一项,按顺序轮播;含「无可抓取内容」与「长内容滚动」项便于验收 */
const SIDE_ITEMS = [
  {
    title: '产品图',
    content: &#96;这是产品主图,左侧会同步展示:

![product](https://picsum.photos/seed/react-bot-side/800/500)&#96;
  },
  {
    title: '产品视频',
    content: &#96;这是产品介绍视频,左侧应解析为可播放的 video:

[演示视频](https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4)

也可直接输出视频地址:

https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4&#96;
  },
  {
    title: '超长单卡',
    content: &#96;左侧仅同步一张超长内容卡片,请验收单卡撑高后的滚动与进出场:

\&#96;\&#96;\&#96;yml
md-components:
  type: LongArticleCard
  props:
    title: 招聘助手使用手册(超长单卡)
    subtitle: 滚动验收
    paragraphs:
      - "本卡片用于验证:当左侧只有一张卡片且内容很长时,侧栏应出现纵向滚动条,而不会把整块对话布局撑破。"
      - "第一章 · 接入准备。确认已安装 @kne/react-bot,并在宿主应用中引入样式与国际化资源。桌面端建议为 ReactBot 外层容器设置明确高度,移动端注意安全区与键盘顶起。"
      - "第二章 · 请求模式。SSE 适合流式打字机体验,Ajax 适合一次性返回。可通过 requestMode、getRequest、parseChunk 自定义协议适配。失败时应展示可重试的错误状态,避免输入框被禁用后无法恢复。"
      - "第三章 · 侧栏能力。开启 openSide 后,图片、视频与 markdownComponents 自定义卡片会同步到左侧。未解析完成的 yml 不应露出原始文本;无可抓取内容时应保持上一条侧栏结果。"
      - "第四章 · 进出场与闪动。侧栏节点指纹未变化时不要频繁重渲染。流式增长可用节流合并刷新;切换到另一条可渲染内容时再执行退场与进场动画。"
      - "第五章 · 输入扩展。MessageInput 支持工具栏扩展、语音输入与 aurora/default 边框。语音 token 过期需重新拉取;Mock 场景可注入 speechTextRealTime 便于演示。"
      - "第六章 · 布局细节。头像固定为圆形 28px,气泡颜色可通过 CSS 变量覆盖。SystemLayout 内嵌时去掉 ReactBot 外层圆角与白底,避免圆角不连续。"
      - "第七章 · 无障碍与性能。长列表应只滚动消息区域;侧栏与对话区滚动相互独立。减少不必要的 Markdown 全量重解析,避免图片闪烁。"
      - "第八章 · 验收清单。1) 单卡超长可滚动 2) 多段内容间距正常 3) 流式不闪动 4) yml 未闭合不露原文 5) 输入框始终可见。完成本卡滚动到底即视为单卡超长场景通过。"
      - "附录 · 占位段落。为了让卡片足够高,这里继续补充说明:候选人筛选、面试日程、JD 生成、Offer 评估都可以作为演示话术。你可以反复发送消息轮播到本项,观察左侧是否始终只有这一张超长卡片,并确认滚到底部仍能看到验收通过字样。"
      - "【验收通过】若你能滚到本段,说明单卡超长场景的侧栏滚动工作正常。"
\&#96;\&#96;\&#96;&#96;
  },
  {
    title: '长内容滚动',
    content: &#96;以下多张图 + 多张卡片会超出左侧可视高度,请验收左侧区域可滚动、不会撑破布局:

![gallery-1](https://picsum.photos/seed/react-bot-scroll-1/800/480)

![gallery-2](https://picsum.photos/seed/react-bot-scroll-2/800/480)

\&#96;\&#96;\&#96;yml
md-components:
  type: ProductCard
  props:
    title: 滚动验收 · 卡片 A
    cover: https://picsum.photos/seed/react-bot-scroll-a/640/360
    price: ¥199/月
    description: 用于验证侧栏内容超出高度时出现纵向滚动条,而不是把整块布局撑破。
    tags:
      - Scroll
      - Side Panel
    onAction: $onViewDetail
\&#96;\&#96;\&#96;

![gallery-3](https://picsum.photos/seed/react-bot-scroll-3/800/520)

\&#96;\&#96;\&#96;yml
md-components:
  type: SpecCard
  props:
    title: 滚动验收 · 规格
    items:
      - label: 预期行为
        value: 左侧独立滚动
      - label: 不应出现
        value: 内容溢出裁切且无滚动条
      - label: 右侧对话
        value: 输入框始终可见
\&#96;\&#96;\&#96;

![gallery-4](https://picsum.photos/seed/react-bot-scroll-4/800/480)

\&#96;\&#96;\&#96;yml
md-components:
  type: ProductCard
  props:
    title: 滚动验收 · 卡片 B
    cover: https://picsum.photos/seed/react-bot-scroll-b/640/360
    price: ¥399/月
    description: 继续拉高侧栏内容,确保滚到底仍能看到本卡片。
    tags:
      - Overflow
      - Accept
    onAction: $onViewDetail
\&#96;\&#96;\&#96;&#96;
  },
  {
    title: '纯文本(无侧栏内容)',
    content: &#96;这是一条普通文字回复,没有图片或自定义组件。

左侧应继续保留上一条已抓取的内容,不会变空。再发一句可看下一项。&#96;
  },
  {
    title: '产品卡片',
    content: &#96;这是自定义 ProductCard,左侧会同步展示:

\&#96;\&#96;\&#96;yml
md-components:
  type: ProductCard
  props:
    title: ReactBot Pro
    cover: https://picsum.photos/seed/react-bot-card/640/360
    price: ¥299/月
    description: 面向业务场景的 AI 对话组件,支持 SSE、侧栏媒体与自定义卡片渲染。
    tags:
      - SSE
      - Side Panel
      - Markdown
    onAction: $onViewDetail
\&#96;\&#96;\&#96;&#96;
  },
  {
    title: '规格卡片',
    content: &#96;这是 SpecCard 规格说明,左侧会同步展示:

\&#96;\&#96;\&#96;yml
md-components:
  type: SpecCard
  props:
    title: 核心能力
    items:
      - label: 请求模式
        value: SSE / Ajax
      - label: 侧栏内容
        value: 图片、视频、自定义组件
      - label: 输入扩展
        value: 工具栏 / 语音识别
\&#96;\&#96;\&#96;&#96;
  },
  {
    title: 'antd Card',
    content: &#96;这是直接注册的 antd Card,左侧会同步展示:

\&#96;\&#96;\&#96;yml
md-components:
  type: Card
  props:
    title: 快速上手
    size: small
    children: 通过 markdownComponents 注册组件后,AI 回复中的 yml 代码块即可渲染为交互卡片,并自动同步到左侧面板。
\&#96;\&#96;\&#96;&#96;
  }
];

const pickSideReply = messages => {
  const userCount = (messages || []).filter(item => item.role === 'user').length;
  const index = Math.max(0, userCount - 1) % SIDE_ITEMS.length;
  const item = SIDE_ITEMS[index];
  const step = index + 1;
  return &#96;【${step}/${SIDE_ITEMS.length} ${item.title}】\n\n${item.content}\n\n再发一句可看下一项。&#96;;
};

const SideChatDemo = () => {
  const { message } = App.useApp();

  return (
    <Flex className="react-bot-example-shell">
      <ReactBot
        title="带侧栏的对话"
        openSide
        ajax={ajax}
        requestMode="sse"
        getRequest={({ messages }) => ({
          url: '/ai/chat-side',
          EventSource: createMockEventSource(pickSideReply(messages))
        })}
        markdownComponents={{
          ProductCard,
          SpecCard,
          LongArticleCard,
          Card,
          Button,
          Tag,
          Descriptions,
          Space
        }}
        markdownProps={{
          variables: {
            onViewDetail: () => {
              message.success('已打开产品详情(示例回调)');
            }
          }
        }}
        messageInputProps={{
          showVoice: true,
          getSpeechToken: mockGetSpeechToken,
          speechTextRealTime: mockSpeechTextRealTime,
          placeholder: '随便说一句,每次只展示一项到左侧'
        }}
        defaultMessages={[
          {
            id: 'welcome',
            role: 'assistant',
            content:
              '开启 openSide 后,初始对话占满宽度;每发一句话轮播:① 产品图 → ② 产品视频 → ③ 超长单卡 → ④ 长内容滚动 → ⑤ 纯文本(保持上一条)→ ⑥ ProductCard → ⑦ SpecCard → ⑧ antd Card。首次有可展示内容后切入左右分栏。'
          }
        ]}
      />
    </Flex>
  );
};

const BaseExample = () => (
  <App>
    <SideChatDemo />
  </App>
);

render(<BaseExample />);
  • ReactBot Ajax
  • 普通 Ajax 与 AI 交互,支持 Mock 语音输入
  • _ReactBot(@kne/current-lib_react-bot)[import * as _ReactBot from "@kne/react-bot"],(@kne/current-lib_react-bot/dist/index.css),_AxiosFetch(@kne/axios-fetch)[import * as _AxiosFetch from "@kne/axios-fetch"],antd(antd)
const { ReactBot } = _ReactBot;
const { default: createAjax } = _AxiosFetch;
const { App, Flex } = antd;

const ajax = createAjax({
  baseURL: 'https://example.com',
  errorHandler: () => {}
});

const mockGetSpeechToken = async () => {
  await new Promise(resolve => setTimeout(resolve, 200));
  return {
    appKey: 'demo-app-key',
    token: 'demo-speech-token',
    expire: Date.now() + 60 * 60 * 1000
  };
};

const mockSpeechTextRealTime = ({ getToken, onChange, onError }) => {
  const fullText = '用 Ajax 模式问一下今天的安排';
  let timer = null;
  let index = 0;
  let stopped = false;
  return Promise.resolve({
    start: async () => {
      try {
        const token = await getToken();
        if (!token || stopped) return;
        timer = setInterval(() => {
          if (stopped) {
            clearInterval(timer);
            return;
          }
          index = Math.min(index + 2, fullText.length);
          onChange && onChange({ message: fullText.slice(0, index) });
          if (index >= fullText.length) {
            clearInterval(timer);
            timer = null;
          }
        }, 60);
      } catch (e) {
        onError && onError(e?.message || 'Mock speech failed');
      }
    },
    stop: () => {
      stopped = true;
      if (timer) {
        clearInterval(timer);
        timer = null;
      }
    }
  });
};

const mockAjax = Object.assign(
  async config => {
    if (config.url === '/ai/ajax-chat') {
      const prompt = config.data?.content || '';
      await new Promise(r => setTimeout(r, 600));
      return {
        data: {
          code: 0,
          data: {
            id: 'ajax-' + Date.now(),
            role: 'assistant',
            content: &#96;(Ajax 模式)已收到:「${prompt}」。这是一次性返回的完整回复。&#96;
          }
        }
      };
    }
    return ajax(config);
  },
  { sse: ajax.sse.bind(ajax), parseUrlParams: ajax.parseUrlParams }
);

const BaseExample = () => {
  return (
    <App>
      <Flex className="react-bot-example-shell">
        <ReactBot
          title="Ajax 模式"
          ajax={mockAjax}
          requestMode="ajax"
          api={{ url: '/ai/ajax-chat', method: 'POST' }}
          messageInputProps={{
            showVoice: true,
            getSpeechToken: mockGetSpeechToken,
            speechTextRealTime: mockSpeechTextRealTime,
            placeholder: '输入消息,或点麦克风使用 Mock 语音'
          }}
          defaultMessages={[
            {
              id: 'welcome',
              role: 'assistant',
              content: '当前为 requestMode="ajax"。可以打字或点麦克风试用 Mock 语音输入。'
            }
          ]}
        />
      </Flex>
    </App>
  );
};

render(<BaseExample />);
  • ReactBot + SystemLayout(全屏)
  • 在 @kne/system-layout 的 aiDialog 中嵌入 ReactBot:桌面端从菜单底部唤起小窗口/内嵌面板,移动端悬浮按钮全屏对话;演示招聘助手 SSE 流式回复与 Mock 语音
  • _ReactBot(@kne/current-lib_react-bot)[import * as _ReactBot from "@kne/react-bot"],(@kne/current-lib_react-bot/dist/index.css),_SystemLayout(@kne/system-layout)[import * as _SystemLayout from "@kne/system-layout"],(@kne/system-layout/dist/index.css),_AxiosFetch(@kne/axios-fetch)[import * as _AxiosFetch from "@kne/axios-fetch"],antd(antd)
const { ReactBot } = _ReactBot;
const { default: SystemLayout, Page } = _SystemLayout;
const { default: createAjax } = _AxiosFetch;
const { App, Flex, Card, Alert, Typography } = antd;
const { Text, Paragraph } = Typography;

const ajax = createAjax({
  baseURL: 'https://example.com',
  errorHandler: () => {}
});

const mockGetSpeechToken = async () => {
  await new Promise(resolve => setTimeout(resolve, 200));
  return {
    appKey: 'demo-app-key',
    token: 'demo-speech-token',
    expire: Date.now() + 60 * 60 * 1000
  };
};

/** Mock 语音转写:无需麦克风,流式输出演示文案 */
const mockSpeechTextRealTime = ({ getToken, onChange, onError }) => {
  const fullText = '帮我筛选高级前端工程师的简历';
  let timer = null;
  let index = 0;
  let stopped = false;
  return Promise.resolve({
    start: async () => {
      try {
        const token = await getToken();
        if (!token || stopped) return;
        timer = setInterval(() => {
          if (stopped) {
            clearInterval(timer);
            return;
          }
          index = Math.min(index + 2, fullText.length);
          onChange && onChange({ message: fullText.slice(0, index) });
          if (index >= fullText.length) {
            clearInterval(timer);
            timer = null;
          }
        }, 60);
      } catch (e) {
        onError && onError(e?.message || 'Mock speech failed');
      }
    },
    stop: () => {
      stopped = true;
      if (timer) {
        clearInterval(timer);
        timer = null;
      }
    }
  });
};

const mockReplies = {
  简历: '已为「高级前端工程师」筛选出 3 位高匹配候选人:\n\n1. **王伟** · 6 年经验 · React/TypeScript · 匹配度 92%\n2. **李静** · 5 年经验 · Vue/Node.js · 匹配度 87%\n3. **张磊** · 4 年经验 · React/微前端 · 匹配度 85%\n\n是否需要我自动发送面试邀约?',
  筛选:
    '已为「高级前端工程师」筛选出 3 位高匹配候选人:\n\n1. **王伟** · 6 年经验 · React/TypeScript · 匹配度 92%\n2. **李静** · 5 年经验 · Vue/Node.js · 匹配度 87%\n3. **张磊** · 4 年经验 · React/微前端 · 匹配度 85%\n\n是否需要我自动发送面试邀约?',
  面试:
    '我可以协助安排面试。候选人王伟本周可预约的时间为:\n\n- 周三 14:00-15:00\n- 周四 10:00-11:00\n\n面试官张总监周三下午有空,建议约在周三 14:00,是否确认发送日程邀请?',
  邀约:
    '我可以协助安排面试。候选人王伟本周可预约的时间为:\n\n- 周三 14:00-15:00\n- 周四 10:00-11:00\n\n面试官张总监周三下午有空,建议约在周三 14:00,是否确认发送日程邀请?',
  jd: '已根据「产品部-高级产品经理」生成 JD 草稿:\n\n**【岗位职责】** 负责 B 端产品全生命周期管理,主导需求调研与方案设计;\n**【任职要求】** 5 年以上 B 端产品经验,熟悉 SaaS 业务,具备良好的数据分析能力。\n\n需要我一键同步到招聘渠道吗?',
  职位:
    '已根据「产品部-高级产品经理」生成 JD 草稿:\n\n**【岗位职责】** 负责 B 端产品全生命周期管理,主导需求调研与方案设计;\n**【任职要求】** 5 年以上 B 端产品经验,熟悉 SaaS 业务,具备良好的数据分析能力。\n\n需要我一键同步到招聘渠道吗?',
  offer:
    '根据候选人当前薪资(月薪 28K)与我司薪酬带宽,建议 Offer 方案:\n\n- 月薪 32K × 15 薪\n- 签字费 20K\n\n该方案在同级别薪酬 P60 分位,具备竞争力。是否生成 Offer 审批单?',
  薪资:
    '根据候选人当前薪资(月薪 28K)与我司薪酬带宽,建议 Offer 方案:\n\n- 月薪 32K × 15 薪\n- 签字费 20K\n\n该方案在同级别薪酬 P60 分位,具备竞争力。是否生成 Offer 审批单?'
};
const defaultReply =
  '我是招聘助手小 K,可以帮你筛选简历、安排面试、生成 JD、评估 Offer。你可以试试发送「帮我筛选前端简历」或点麦克风使用 Mock 语音。';

function createMockEventSource(fullText) {
  let index = 0;
  let timer = null;
  const listeners = {};
  const messageId = 'assistant-layout-' + Date.now();

  const MockES = function () {
    this.readyState = 0;
    this.onopen = null;
    this.onmessage = null;
    this.onerror = null;

    setTimeout(() => {
      this.readyState = 1;
      if (typeof this.onopen === 'function') this.onopen({ type: 'open' });
      timer = setInterval(() => {
        if (this.readyState !== 1) return;
        if (index >= fullText.length) {
          clearInterval(timer);
          timer = null;
          this._dispatch('message', JSON.stringify({ id: messageId, content: '', done: true }));
          this.readyState = 2;
          return;
        }
        const chunkSize = Math.floor(Math.random() * 3) + 1;
        const chunk = fullText.slice(index, index + chunkSize);
        index += chunkSize;
        this._dispatch('message', JSON.stringify({ id: messageId, content: chunk, done: false }));
      }, 30);
    }, 200);
  };

  MockES.CONNECTING = 0;
  MockES.OPEN = 1;
  MockES.CLOSED = 2;
  MockES.prototype.addEventListener = function (type, handler) {
    if (!listeners[type]) listeners[type] = [];
    listeners[type].push(handler);
  };
  MockES.prototype._dispatch = function (type, data) {
    const event = { type, data };
    if (type === 'message' && typeof this.onmessage === 'function') this.onmessage(event);
    (listeners[type] || []).forEach(handler => handler(event));
  };
  MockES.prototype.close = function () {
    this.readyState = 2;
    if (timer) clearInterval(timer);
  };
  return MockES;
}

function getMockReply(input) {
  const key = Object.keys(mockReplies).find(k => input.toLowerCase().includes(k));
  return key ? mockReplies[key] : defaultReply;
}

const menu = {
  base: '/SystemLayout',
  items: [
    { path: '/', label: 'Onboarding', toolbar: true, icon: ({ active }) => (active ? 'home' : 'home_line') },
    { group: 'HIRING', path: '/hiring', label: 'Hiring Hub', toolbar: true, icon: 'icon-assignment_ind' },
    { group: 'HIRING', path: '/hiring/application', label: 'Application List', icon: 'icon-assignment' },
    { group: 'PEOPLE', path: '/people', label: 'Management', toolbar: true, icon: 'icon-automation' }
  ]
};

const BaseExample = () => {
  return (
    <App>
      <SystemLayout
        userInfo={{ name: 'Lucy L', email: '[email protected]' }}
        menu={menu}
        aiDialog={{
          title: '招聘助手 · 小 K',
          content: (
            <ReactBot
              className="react-bot-system-layout-dialog"
              ajax={ajax}
              requestMode="sse"
              api={{ url: '/ai/chat' }}
              getRequest={({ content }) => ({
                url: '/ai/chat',
                params: { prompt: content },
                EventSource: createMockEventSource(getMockReply(content))
              })}
              messageInputProps={{
                showVoice: true,
                borderType: 'default',
                getSpeechToken: mockGetSpeechToken,
                speechTextRealTime: mockSpeechTextRealTime,
                placeholder: '输入问题,或点麦克风使用 Mock 语音',
                autoSize: { minRows: 1, maxRows: 3 }
              }}
              defaultMessages={[
                {
                  id: 'welcome',
                  role: 'assistant',
                  content:
                    '你好,我是招聘助手小 K。本周你有 3 个职位在招、8 份简历待筛选。需要我帮你做点什么吗?'
                }
              ]}
            />
          )
        }}
      >
        <Page title="AI 招聘助手">
          <Flex vertical gap={16}>
            <Alert
              type="info"
              showIcon
              message="在 SystemLayout 上接入 ReactBot"
              description={
                <span>
                  通过 <code>aiDialog.content</code> 放入 <code>ReactBot</code> 即可。
                  <br />
                  <b>桌面端</b>:点击左侧菜单底部的 AI 入口;支持小窗口(small)与内嵌面板(inner)。
                  <br />
                  <b>移动端</b>:点击右下角可拖动悬浮按钮,全屏展示对话框。
                </span>
              }
            />
            <Card title="招聘助手能做什么" styles={{ body: { padding: 20 } }} style={{ background: 'rgba(255,255,255,0.5)' }}>
              <Flex vertical gap={12}>
                <Paragraph style={{ margin: 0 }}>
                  本示例演示如何在 <code>@kne/system-layout</code> 的 AI 对话框中嵌入 <code>ReactBot</code>,获得 SSE
                  流式回复、Markdown 渲染与 Mock 语音输入能力。
                </Paragraph>
                <Flex vertical gap={8}>
                  <Text>· 智能筛选简历,按岗位匹配度排序推荐候选人</Text>
                  <Text>· 协调面试官与候选人的空闲时间,一键发送日程邀约</Text>
                  <Text>· 根据部门和职级自动生成岗位 JD 草稿</Text>
                  <Text>· 结合薪酬带宽评估 Offer 方案,生成审批单</Text>
                </Flex>
                <Text type="secondary">提示:试着发送「帮我筛选前端简历」或点麦克风试用 Mock 语音。</Text>
              </Flex>
            </Card>
          </Flex>
        </Page>
      </SystemLayout>
    </App>
  );
};

render(<BaseExample />);

API

MessageList

消息列表,按 role 左右分布,支持头像、姓名、气泡内容与底部操作扩展,也可用于 IM 对话展示。

属性

| 属性 | 类型 | 默认值 | 描述 | |------|------|-------|------| | messages | ChatMessage[] | [] | 消息列表 | | roles | RolesConfig | 内置 user/assistant/system | role 展示配置(placement/avatar/name) | | markdown | boolean | true | 字符串内容是否用 Markdown 渲染 | | markdownProps | object | - | 透传给 @kne/markdown-components-render | | title | ReactNode | - | 列表顶部标题;不传则不显示该区域 | | renderAvatar | function | - | 自定义头像 (message, roleConfig) => ReactNode | | renderName | function | - | 自定义姓名 | | renderContent | function | - | 自定义气泡内容 | | renderFooter | function | - | 消息底部操作扩展 | | renderStatus | function | - | 自定义状态展示 | | className | string | - | 自定义类名 |

CSS 变量

可通过消息 / role 的 style,或外层容器覆盖:

| 变量 | 默认值 | 描述 | |------|-------|------| | --message-bubble-bg-left | #f6f6f6 | 左侧(assistant/system)气泡背景 | | --message-bubble-color-left | inherit | 左侧气泡文字颜色 | | --message-bubble-bg-right | #e8f2ff | 右侧(user)气泡背景 | | --message-bubble-color-right | inherit | 右侧气泡文字颜色 |

<MessageList
  roles={{
    assistant: { style: { '--message-bubble-bg-left': '#fff' } },
    user: { style: { '--message-bubble-bg-right': '#dbeafe' } }
  }}
  messages={messages}
/>

MessageInput

用户输入框:顶部可扩展工具栏、文本框、发送按钮、语音输入(@kne/speech-text)。

属性

| 属性 | 类型 | 默认值 | 描述 | |------|------|-------|------| | value | string | - | 受控文本 | | defaultValue | string | '' | 非受控初始值 | | onChange | function | - | 文本变化回调 | | onSend | function | - | 发送回调 (text) => void | | disabled | boolean | - | 禁用 | | loading | boolean | - | 加载中 | | placeholder | string | 国际化文案 | 占位符 | | autoSize | object|boolean | {minRows:1,maxRows:6} | TextArea 自适应 | | toolbarItems | ToolbarItem[] | [] | 顶部工具栏项(表情、图片、通话、附件等可扩展) | | toolbarExtra | ReactNode | - | 工具栏额外内容 | | showVoice | boolean | true | 是否显示语音输入 | | getSpeechToken | function | - | 语音识别 token:() => Promise<{appKey,token,expire?}> | | speechOptions | object | - | 透传 speechTextRealTime 配置 | | speechTextRealTime | function | @kne/speech-text 默认实现 | 可注入 Mock 引擎,签名同 speechTextRealTime | | sendIcon | ReactNode | SendOutlined | 发送按钮图标 | | extra | ReactNode | - | 操作区额外内容 | | borderType | 'default'\|'aurora' | 'aurora' | 边框样式:普通边框 / 炫彩滚动边框 | | auroraProps | object | - | 透传炫彩边框配置(radius/ringWidth/flowSpeed/lineLength/primaryColor/accentColor/animated) |

CSS 变量

borderType="aurora" 时由 auroraProps 写入,也可在外层覆盖:

| 变量 | 默认值 | 描述 | |------|-------|------| | --message-input-radius | 12px | 输入框圆角 | | --message-input-ring-width | 2px | 炫彩描边宽度 | | --message-input-primary | #0061f2 | 炫彩主色 | | --message-input-accent | #ff7ad2 | 炫彩辅色 | | --message-input-pulse-duration | 2.8s | 光晕呼吸动画周期 |

语音示例见 MessageInput Voice:示例内通过 speechTextRealTime 注入 Mock 流式转写,无需麦克风。生产环境使用默认实现并配置真实 getSpeechToken

ReactBot

组合 MessageList + MessageInput,通过 @kne/axios-fetch 的 SSE 或普通 Ajax 与 AI 交互;使用 @kne/markdown-components-render 渲染回复;支持左右结构将 AI 部分内容在左侧实时展示(openSide,默认关闭)。

属性

| 属性 | 类型 | 默认值 | 描述 | |------|------|-------|------| | ajax | function | - | createAjax 实例(必填,用于发消息) | | requestMode | 'sse'\|'ajax' | 'sse' | 请求模式 | | api | object | - | 请求配置模板 | | getRequest | function | - | 动态生成请求配置 | | parseChunk | function | defaultParseChunk | SSE 分片解析 | | parseResponse | function | defaultParseResponse | Ajax 响应解析 | | EventSource | class | - | 自定义/Mock EventSource | | messages | ChatMessage[] | - | 受控消息列表 | | defaultMessages | ChatMessage[] | [] | 非受控初始消息 | | onMessagesChange | function | - | 消息变化回调 | | openSide | boolean | false | 是否开启左侧内容面板 | | sideSizes | array | - | 受控分栏尺寸;传入后不写 localStorage | | defaultSideSizes | array | ['50%','50%'] | 默认分栏尺寸(无缓存且非受控时使用) | | onSideResize | function | - | 分栏拖拽回调 | | sideHtmlTransform | function | defaultSideHtmlTransform | 左侧内容 HTML 过滤 | | sideEmpty | ReactNode | 国际化文案 | 左侧空状态 | | markdown | boolean | true | 是否 Markdown 渲染 | | markdownProps | object | - | Markdown 渲染配置 | | markdownComponents | object | - | Markdown 自定义组件映射 | | roles | RolesConfig | - | 透传 MessageList | | messageListProps | object | - | 透传 MessageList | | messageInputProps | object | - | 透传 MessageInput | | title | ReactNode | - | 标题栏;不传则不显示该区域 | | headerExtra | ReactNode | - | 标题栏右侧扩展(需同时传 title 才会显示标题栏) | | footerExtra | ReactNode | - | 输入区下方扩展 | | disabled | boolean | - | 禁用输入 | | className | string | - | 自定义类名 |

气泡 / 输入框主题可通过 CSS 变量覆盖(见 MessageList、MessageInput 的 CSS 变量);例如在 className 对应样式中设置 --message-bubble-bg-left,或经 roles / messageListProps / messageInputProps.auroraProps 传入。

开启 openSide 时:桌面为左右分栏,对话区用 stripSideContentFromMarkdown + defaultOmitSideHtmlTransform 剔除侧栏内容。移动端不拆栏、不抽取:媒体/组件留在气泡内渲染(normalizeSideMarkdownForRender 去掉未解析 yml,defaultInlineMediaHtmlTransform 解析图片/视频)。非受控分栏尺寸默认 50%/50%,拖拽后写入 localStoragereact-bot:side-sizes);传入 sideSizes 则为受控模式且不写缓存。

@kne/system-layout 集成:将 ReactBot 作为 aiDialog.content 传入即可(见示例 ReactBot + SystemLayout)。布局侧已提供对话框标题,一般无需再传 ReactBottitle。嵌入时父级需为纵向 flex 且能撑满高度(display:flex; flex-direction:column; min-height:0,如 .page-window-content),ReactBotflex:1 1 0% 撑满,消息在内部滚动;可用 className 去掉默认白底以适配对话框磨砂背景。

导出

| 名称 | 说明 | |------|------| | ReactBot / default | 主聊天组件 | | MessageList | 消息列表 | | MessageInput | 输入框 | | VoiceInput | 语音输入 | | SidePanel | 左侧面板 | | useChatRequest | 聊天请求 Hook | | defaultParseChunk / defaultParseResponse | 默认解析 | | defaultSideHtmlTransform | 默认左侧 HTML 过滤 | | defaultOmitSideHtmlTransform | 开启侧栏时对话区剔除左侧内容 | | defaultInlineMediaHtmlTransform | 无侧栏时对话区解析图片/视频链接与裸 URL | | hasSidePanelContent | 判断 markdown 是否含可提取的侧栏内容(可含流式中) | | stripSideContentFromMarkdown | 从对话 markdown 剔除侧栏内容(含未完成片段) | | DEFAULT_ROLES / resolveRoleConfig | role 工具 | | withLocale | 国际化 HOC |