MarkdownInputField - 输入框

MarkdownInputField 是一个带发送功能的 Markdown 输入字段组件,允许用户编辑 Markdown 内容并通过按钮或快捷键发送。

功能特点

  • 📝 支持 Markdown 输入
  • 📎 支持附件上传
  • 🔘 支持自定义操作按钮
  • 🍵 支持插槽输入
  • 🎯 支持技能模式

import { Button, message } from 'antd';
import { AimOutlined, GlobalOutlined, EditOutlined } from '@ant-design/icons';
import { Sparkles, ChevronDown } from '@sofa-design/icons';
import {
  ActionItemBox,
  ActionItemContainer,
  MarkdownInputField,
  SuggestionList,
  ActionIconBox,
  CreateRecognizer,
  type MarkdownEditorInstance,
} from '@ant-design/agentic-ui';

const createRecognizer: CreateRecognizer = async ({ onPartial, onError }) => {
  let timer: ReturnType<typeof setInterval>;
  return {
    start: async () => {
      // 真实场景应启动麦克风与ASR服务,这里仅用计时器模拟持续的转写片段
      let i = 0;
      timer = setInterval(() => {
        onPartial(`语音片段${i} `);
        i += 1;
      }, 500);
    },
    stop: async () => {
      clearInterval(timer);
    },
  };
};
export default () => {
  const [value, setValue] = React.useState(
    '`${placeholder:目标场景}`今天的拒绝率为什么下降`${placeholder:目标事件}`输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本',
  );

  const markdownRef = React.useRef<MarkdownEditorInstance>(null);

  return (
    <div
      style={{
        boxSizing: 'border-box',
        margin: '0 auto',
        maxWidth: 920,
        padding: '0 12px',
        width: '100%',
      }}
    >
      <MarkdownInputField
        style={{ maxWidth: '100%' }}
        value={value}
        inputRef={markdownRef}
        voiceRecognizer={createRecognizer}
        attachment={{
          enable: true,
          accept: '.pdf,.doc,.docx,image/*',
          maxFileSize: 10 * 1024 * 1024, // 10MB(字节)
          upload: async (file, index) => {
            if (index == 3) {
              throw new Error('上传失败');
            }
            // 模拟上传文件
            await new Promise((resolve) => setTimeout(resolve, 1000));
            return URL.createObjectURL(file);
          },
          onDelete: async (file) => {
            console.log('删除文件:', file);
            await new Promise((resolve) => setTimeout(resolve, 500));
          },
        }}
        tagInputProps={{
          type: 'dropdown',
          enable: true,
          items: async (props) => {
            if (props?.placeholder === '目标场景') {
              return [];
            }
            return ['tag1', 'tag2', 'tag3'].map((item) => {
              return {
                key: item,
                label: props?.placeholder + item,
              };
            });
          },
        }}
        actionsRender={(props, defaultActions) => {
          return [
            <ActionIconBox
              showTitle={props.collapseSendActions}
              title="提示词库"
              key="edit"
              style={{
                padding: 8,
                fontSize: 16,
              }}
            >
              <Sparkles />
            </ActionIconBox>,
            ...defaultActions,
          ];
        }}
        beforeToolsRender={() => {
          return (
            <ActionItemContainer showMenu={true}>
              {new Array(6).fill(0).map((_, index) => (
                <ActionItemBox
                  onClick={() => message.info('快捷技能' + index)}
                  icon="https://mdn.alipayobjects.com/huamei_ptjqan/afts/img/A*Bgr8QrMHLvoAAAAAF1AAAAgAekN6AQ/original"
                  iconSize={16}
                  size="small"
                  title={
                    <span
                      style={{ display: 'flex', alignItems: 'center', gap: 8 }}
                    >
                      {'快捷技能' + index}
                    </span>
                  }
                  disabled={index < 2}
                  key={'快捷技能' + index}
                />
              ))}
            </ActionItemContainer>
          );
        }}
        toolsRender={() => [
          <Button
            type="text"
            key="bold"
            onClick={() => console.log('DeepThink clicked')}
          >
            DeepThink
            <ChevronDown />
          </Button>,
          <Button
            type="text"
            key="italic"
            icon={<GlobalOutlined />}
            onClick={() => console.log('深度思考 clicked')}
          >
            深度思考
          </Button>,
          <Button
            type="text"
            key="link"
            icon={<AimOutlined />}
            onClick={() => console.log('联网搜索 clicked')}
          >
            联网搜索
          </Button>,
        ]}
        onChange={(newValue) => {
          setValue(newValue);
          console.log('newValue', newValue);
        }}
        placeholder="请输入内容..."
        onSend={async (text) => {
          console.log('发送内容:', text);
          await new Promise((resolve) => setTimeout(resolve, 1000));
        }}
      />
      <SuggestionList
        style={{
          marginTop: 8,
          maxWidth: '100%',
        }}
        items={[
          {
            key: 'qwe',
            icon: '💸',
            text: '关税对消费类基金的影响',
            actionIcon: <EditOutlined />,
          },
          {
            key: 'asd',
            icon: '📝',
            text: '恒生科技指数基金相关新闻',
            actionIcon: <EditOutlined />,
          },
          {
            key: 'zxc',
            icon: '📊',
            text: '数据分析与可视化',
            actionIcon: <EditOutlined />,
          },
        ]}
        layout={'horizontal'}
        onItemClick={() => {
          markdownRef?.current?.store?.setMDContent(
            '关税对`${placeholder:消费类}`基金的影响',
          );
        }}
      />
    </div>
  );
};

快捷键

组件支持以下快捷键操作,可以通过 triggerSendKey 属性配置发送行为。

发送与换行

模式发送快捷键换行快捷键适用场景
'Enter' (默认)EnterShift + Enter适合短文本对话,按回车直接发送
'Mod+Enter'Cmd / Ctrl + EnterEnter适合长文本编辑,需要频繁换行

特殊说明:

  1. 移动端适配:在移动设备上,为防止误触,强制使用 'Mod+Enter' 模式(即点击键盘回车键仅换行,不发送)。
  2. 输入法兼容:在中文输入法(IME)组字/选词过程中,按 Enter 键不会触发发送。

输入历史导航

开启 inputHistory.enable 后(对齐桌面 IDE 聊天输入框行为):

快捷键行为
↑光标位于内容开头时,回溯上一条已发送内容
↓光标位于内容末尾时,前进到下一条(回到栈顶时找回编辑中内容)
  • 发送成功的内容自动进入历史栈(去重、上限默认 100 条,可配 maxLength);
  • 回溯前当前编辑中的内容会被暂存,按 ↓ 到栈顶可找回;
  • 历史仅保存在组件实例内存中,不跨会话持久化。

拖拽上传

配置 attachment.enable 且提供 upload / uploadWithResponse 后,直接把文件拖入输入框即可上传(含格式、大小、数量校验),拖拽悬停时会显示高亮覆盖层。

<MarkdownInputField
  attachment={{
    enable: true,
    accept: '.pdf,.doc,.docx,image/*',
    upload: async (file) => URL.createObjectURL(file),
  }}
/>

编辑器通用快捷键

除了发送快捷键外,组件还支持以下 Markdown 编辑常用的快捷键:

快捷键 (Mac / Windows)功能
Cmd + B / Ctrl + B加粗
Cmd + I / Ctrl + I斜体
Cmd + Shift + S / Ctrl + Shift + S删除线
Option + </kbd> / <kbd>Alt</kbd> + <kbd>行内代码
Cmd + \ / Ctrl + \清除格式
Cmd + 14 / Ctrl + 14标题 H1 ~ H4
Cmd + 0 / Ctrl + 0转换为普通段落
Cmd + ] / Ctrl + ]增加标题级别(变小)
Cmd + [ / Ctrl + [降低标题级别(变大)
Option + Q / Alt + Q引用块
Cmd + Opt + O / Ctrl + Alt + O有序列表
Cmd + Opt + U / Ctrl + Alt + U无序列表
Cmd + Opt + S / Ctrl + Alt + S任务列表
Cmd + Opt + C / Ctrl + Alt + C代码块
Cmd + Opt + T / Ctrl + Alt + T表格
Cmd + Opt + / / Ctrl + Alt + /分割线
Cmd + Shift + L / Ctrl + Shift + L选择当前行
Cmd + D / Ctrl + D选择当前单词/汉字
Cmd + Z / Ctrl + Z撤销
Cmd + Shift + Z / Ctrl + Shift + Z重做

API

属性说明类型默认值版本
value当前的 markdown 文本值string--
onChange当输入值改变时触发的回调函数(value: string) => void--
placeholder输入字段的占位文本string--
style应用于输入字段的内联样式React.CSSProperties--
className应用于输入字段的 CSS 类名string--
disabled是否禁用输入字段boolean--
typingAI 回复中等场景下为 true,输入区只读并显示提示boolean--
allowEmptySubmit是否允许在内容为空时也触发发送booleanfalse-
triggerSendKey触发发送操作的键盘快捷键'Enter' | 'Mod+Enter''Enter'-
onSend当内容发送时触发的异步回调函数(value: string) => Promise<void>--
onStop正在输入中时点击发送按钮的回调函数() => void--
onFocus当输入字段获得焦点时触发的回调函数(value: string, schema: Elements[], e: FocusEvent) => void--
onBlur当输入字段失去焦点时触发的回调函数(value: string, schema: Elements[], e: MouseEvent) => void--
tagInputProps标签输入的相关属性MarkdownEditorProps['tagInputProps']--
suggestionProps内部建议下拉框配置;可关闭下拉框或指定弹层容器SuggestionProps--
borderRadius边框圆角大小number12-
attachment附件配置{ enable?: boolean } & AttachmentButtonProps--
voiceRecognizer语音输入配置CreateRecognizer--
actionsRender自定义渲染操作按钮的函数(props, defaultActions) => React.ReactNode[]--
toolsRender自定义渲染操作按钮前内容的函数(props) => React.ReactNode[]--
beforeToolsRender自定义渲染输入框上方的工具栏区域(props) => React.ReactNode--
quickActionRender自定义右上操作按钮渲染函数(props) => React.ReactNode[]--
leafRender自定义叶子节点渲染函数(props, defaultDom) => React.ReactElement--
inputRef输入框引用React.MutableRefObject<MarkdownEditorInstance>--
skillMode技能模式配置SkillModeConfig--
onSkillModeOpenChange技能模式状态变化回调(open: boolean) => void--
pasteConfig粘贴配置{ enabled?: boolean; allowedTypes?: string[] }--
refinePrompt提示词优化配置{ enable: boolean; onRefine: (input: string) => Promise<string>; }--
enlargeable放大功能配置{ enable?: boolean; height?: number; }--
isShowTopOperatingArea是否显示顶部操作区域booleanfalse-
targetRef顶部操作区域回到顶部/底部功能的目标元素引用React.RefObject<HTMLDivElement>--
operationBtnRender顶部操作区域自定义操作按钮渲染函数() => React.ReactNode--
isShowBackTo是否在顶部操作区域显示回到顶部/底部按钮booleantrue-
maxHeight输入框的最大高度number | string--
maxLength输入文本的最大字符数限制number--
onMaxLengthExceeded当输入达到最大长度限制时的回调函数(value: string) => void--
sendButtonProps发送按钮配置SendButtonCustomizationProps--
disableHoverAnimation是否禁用 hover 动画booleanfalse-
disableFocusAnimation是否禁用聚焦边框光束动画booleanfalse-
inputHistory输入历史导航配置(↑/↓ 翻阅已发送内容){ enable?: boolean; maxLength?: number }--
followups建议问题配置(输入框下方可点击发送/回填){ items?: { text: string; fillOnly?: boolean; icon?: ReactNode; title?: string }[] }--
bgColorList背景颜色列表string[]--
composerChips内联原子 chip 体系(slash 命令 / @file / @symbol){ enable?: boolean; onSlashChipGate?; onSlashChipClick? }-2.33.0
longTextFold长文本粘贴折叠(≥1200 字符或 ≥12 行折叠为 chip){ enable?: boolean }{ enable: false }2.33.0
draftfragment 级草稿恢复(idle/blur/send/switch 提交){ draftKey?; storage?; idleDelay?; onDraftCommit?; onDraftRestore? }-2.33.0
contextUsage上下文用量环形指示器{ usedTokens: number; contextWindow: number; ... }-2.33.0
branch分支选择触发器(分支 pill + 搜索菜单){ branchName?; branches?; onSelectBranch?; ... }-2.33.0

类型定义

AttachmentButtonProps

附件按钮配置属性。

属性说明类型默认值版本
upload文件上传处理函数,返回文件 URL(file: AttachmentFile, index: number) => Promise<string>--
uploadWithResponse文件上传处理函数(返回完整响应),优先级高于 upload(file: AttachmentFile, index: number) => Promise<UploadResponse>--
fileMap文件映射表,用于存储已上传的文件Map<string, AttachmentFile>--
onFileMapChange文件映射表变更时的回调(files?: Map<string, AttachmentFile>) => void--
supportedFormat支持的文件格式配置SupportedFileFormats--
disabled是否禁用按钮boolean--
maxFileSize单个文件最大大小(字节)number--
maxFileCount最大文件数量number--
allowMultiple是否允许一次选择多个文件(默认:true)boolean--
onExceedMaxCount文件数量超出 maxFileCount 限制时的回调(info: { maxCount: number; currentCount: number; selectedCount: number }) => void--
onPreview自定义文件预览处理函数,不传时图片默认弹窗预览、其他文件新窗口打开(file: AttachmentFile) => void | Promise<void>--

SkillModeConfig

技能模式配置接口。

属性说明类型默认值版本
enable是否启用技能模式组件,当为 false 时组件完全不渲染booleantrue-
open是否打开技能模式booleanfalse-
title技能模式标题React.ReactNode--
rightContent右侧自定义内容,支持单个节点或数组React.ReactNode | React.ReactNode[]--
closable是否显示默认关闭按钮booleantrue-
style技能模式容器样式React.CSSProperties--
className技能模式容器类名string--

SendButtonCustomizationProps

发送按钮自定义配置。

属性说明类型默认值版本
compact是否使用紧凑模式显示按钮boolean--
colors自定义按钮颜色配置SendButtonColors--
disabled是否禁用发送按钮。显式传入时优先级高于内部上传状态判断;未传入时,文件上传中会自动禁用boolean--

SendButtonColors

发送按钮颜色配置。

属性说明类型默认值版本
icon默认状态下的图标颜色string--
iconHoverHover 状态下的图标颜色string--
background默认状态下的背景颜色string--
backgroundHoverHover 状态下的背景颜色string--

示例

基础使用

import { MarkdownInputField } from '@ant-design/agentic-ui';
import { Button } from 'antd';
import { ChevronDown } from '@sofa-design/icons';

const App = () => {
  const [value, setValue] = React.useState('');

  return (
    <div
      style={{
        padding: 12,
        display: 'flex',
        flexDirection: 'column',
        gap: 12,
      }}
    >
      <MarkdownInputField
        value={value}
        toolsRender={() => [
          <Button
            type="text"
            key="bold"
            onClick={() => console.log('DeepThink clicked')}
          >
            DeepThink
            <ChevronDown />
          </Button>,
          <Button
            type="text"
            key="italic"
            onClick={() => console.log('深度思考 clicked')}
          >
            深度思考
          </Button>,
          <Button
            type="text"
            key="link"
            onClick={() => console.log('联网搜索 clicked')}
          >
            联网搜索
          </Button>,
        ]}
        onChange={(newValue) => setValue(newValue)}
        placeholder="请输入内容..."
        onSend={async (text) => {
          console.log('发送内容:', text);
          await new Promise((resolve) => setTimeout(resolve, 1000));
        }}
      />
      <MarkdownInputField
        value={value}
        onChange={(newValue) => setValue(newValue)}
        placeholder="请输入内容..."
        onSend={async (text) => {
          console.log('发送内容:', text);
          await new Promise((resolve) => setTimeout(resolve, 1000));
        }}
      />
      <div>
        <h4>Props 说明</h4>
        <ul>
          <li>
            <code>value</code> - 当前的 markdown 文本值
          </li>
          <li>
            <code>onChange</code> - 当输入值改变时触发的回调函数
          </li>
          <li>
            <code>placeholder</code> - 输入字段的占位文本
          </li>
          <li>
            <code>onSend</code> - 当内容发送时触发的异步回调函数
          </li>
        </ul>
      </div>
    </div>
  );
};
export default App;

AI 回复中的输入指引

当 typing 为 true(例如 AI 正在流式输出)或发送中的 onSend 尚未 resolve,且当前输入为空时,输入区左侧会显示带动画的提示文案(国际化键 input.typing.hint),比仅依赖发送按钮 loading 更明显。typing 为 true 时输入区为只读,无法编辑或语音输入,附件按钮亦不可用。

小屏幕

import { Button, message } from 'antd';
import { AimOutlined, GlobalOutlined, EditOutlined } from '@ant-design/icons';
import { Sparkles, ChevronDown } from '@sofa-design/icons';
import {
  ActionItemBox,
  ActionItemContainer,
  MarkdownInputField,
  SuggestionList,
  ActionIconBox,
  CreateRecognizer,
  type MarkdownEditorInstance,
} from '@ant-design/agentic-ui';

const createRecognizer: CreateRecognizer = async ({ onPartial, onError }) => {
  let timer: ReturnType<typeof setInterval>;
  return {
    start: async () => {
      // 真实场景应启动麦克风与ASR服务,这里仅用计时器模拟持续的转写片段
      let i = 0;
      timer = setInterval(() => {
        onPartial(`语音片段${i} `);
        i += 1;
      }, 500);
    },
    stop: async () => {
      clearInterval(timer);
    },
  };
};
export default () => {
  const [value, setValue] = React.useState(
    '`${placeholder:目标场景}`今天的拒绝率为什么下降`${placeholder:目标事件}`输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本效果,输入多行文本',
  );

  const markdownRef = React.useRef<MarkdownEditorInstance>(null);

  return (
    <div
      style={{
        boxSizing: 'border-box',
        border: '1px solid #eee',
        margin: 'auto',
        maxWidth: 460,
        overflow: 'hidden',
        padding: 16,
        width: '100%',
      }}
    >
      <MarkdownInputField
        style={{ maxWidth: '100%' }}
        value={value}
        inputRef={markdownRef}
        voiceRecognizer={createRecognizer}
        attachment={{
          enable: true,
          accept: '.pdf,.doc,.docx,image/*',
          maxFileSize: 10 * 1024 * 1024, // 10MB(字节)
          upload: async (file, index) => {
            if (index == 3) {
              throw new Error('上传失败');
            }
            // 模拟上传文件
            await new Promise((resolve) => setTimeout(resolve, 1000));
            return URL.createObjectURL(file);
          },
          onDelete: async (file) => {
            console.log('删除文件:', file);
            await new Promise((resolve) => setTimeout(resolve, 500));
          },
        }}
        tagInputProps={{
          type: 'dropdown',
          enable: true,
          items: async (props) => {
            if (props?.placeholder === '目标场景') {
              return [];
            }
            return ['tag1', 'tag2', 'tag3'].map((item) => {
              return {
                key: item,
                label: props?.placeholder + item,
              };
            });
          },
        }}
        actionsRender={(props, defaultActions) => {
          return [
            <ActionIconBox
              showTitle={props.collapseSendActions}
              title="提示词库"
              key="edit"
              style={{
                padding: 8,
                fontSize: 16,
              }}
            >
              <Sparkles />
            </ActionIconBox>,
            ...defaultActions,
          ];
        }}
        beforeToolsRender={() => {
          return (
            <ActionItemContainer showMenu={true}>
              {new Array(6).fill(0).map((_, index) => (
                <ActionItemBox
                  onClick={() => message.info('快捷技能' + index)}
                  icon="https://mdn.alipayobjects.com/huamei_ptjqan/afts/img/A*Bgr8QrMHLvoAAAAAF1AAAAgAekN6AQ/original"
                  iconSize={16}
                  size="small"
                  title={
                    <span
                      style={{ display: 'flex', alignItems: 'center', gap: 8 }}
                    >
                      {'快捷技能' + index}
                    </span>
                  }
                  disabled={index < 2}
                  key={'快捷技能' + index}
                />
              ))}
            </ActionItemContainer>
          );
        }}
        toolsRender={() => [
          <Button
            type="text"
            key="bold"
            onClick={() => console.log('DeepThink clicked')}
          >
            DeepThink
            <ChevronDown />
          </Button>,
          <Button
            type="text"
            key="italic"
            icon={<GlobalOutlined />}
            onClick={() => console.log('深度思考 clicked')}
          >
            深度思考
          </Button>,
        ]}
        onChange={(newValue) => {
          setValue(newValue);
          console.log('newValue', newValue);
        }}
        placeholder="请输入内容..."
        onSend={async (text) => {
          console.log('发送内容:', text);
          await new Promise((resolve) => setTimeout(resolve, 1000));
        }}
      />
      <SuggestionList
        style={{
          marginTop: 8,
          maxWidth: '100%',
        }}
        items={[
          {
            key: 'qwe',
            icon: '💸',
            text: '关税对消费类基金的影响',
            actionIcon: <EditOutlined />,
          },
          {
            key: 'asd',
            icon: '📝',
            text: '恒生科技指数基金相关新闻',
            actionIcon: <EditOutlined />,
          },
          {
            key: 'zxc',
            icon: '📊',
            text: '数据分析与可视化',
            actionIcon: <EditOutlined />,
          },
        ]}
        layout={'horizontal'}
        onItemClick={() => {
          markdownRef?.current?.store?.setMDContent(
            '关税对`${placeholder:消费类}`基金的影响',
          );
        }}
      />
    </div>
  );
};

启用语音输入按钮(支持句级回调)

import { Button } from 'antd';
import {
  MarkdownInputField,
  type CreateRecognizer,
} from '@ant-design/agentic-ui';
import { ChevronDown } from '@sofa-design/icons';
export default () => {
  const createRecognizer: CreateRecognizer = async ({
    onSentenceBegin,
    onPartial,
    onSentenceEnd,
    onError,
  }) => {
    let timer: ReturnType<typeof setInterval>;
    let i = 0;
    return {
      start: async () => {
        // 真实场景应启动麦克风与ASR服务,这里用计时器模拟:句子开始 -> 多次增量 -> 句子结束
        onSentenceBegin();
        timer = setInterval(() => {
          if (i < 3) {
            onPartial(`片段${i}`);
            i += 1;
          } else {
            clearInterval(timer);
            onSentenceEnd('完整句子');
          }
        }, 500);
      },
      stop: async () => {
        clearInterval(timer);
      },
    };
  };

  return (
    <MarkdownInputField
      placeholder="请开始讲话..."
      toolsRender={() => [
        <Button
          type="text"
          key="bold"
          onClick={() => console.log('DeepThink clicked')}
        >
          DeepThink
          <ChevronDown />
        </Button>,
        <Button
          type="text"
          key="italic"
          onClick={() => console.log('深度思考 clicked')}
        >
          深度思考
        </Button>,
        <Button
          type="text"
          key="link"
          onClick={() => console.log('联网搜索 clicked')}
        >
          联网搜索
        </Button>,
      ]}
      voiceRecognizer={createRecognizer}
      onChange={(a) => console.log(a)}
      onSend={async (text) => {
        console.log('发送内容:', text);
      }}
    />
  );
};

交互说明:

  • 第一次点击语音按钮开始录音,实时将转写文本写入输入框。
  • 再次点击语音按钮结束录音。
  • 录音过程中点击发送按钮将先停止录音,再发送当前输入内容。

自定义触发键和样式

import { MarkdownInputField } from '@ant-design/agentic-ui';
export default () => {
  const [value, setValue] = React.useState('');
  return (
    <div>
      <MarkdownInputField
        value={value}
        onChange={setValue}
        placeholder="按Enter发送消息,Shift+Enter换行..."
        triggerSendKey="Enter"
        style={{ minHeight: '200px' }}
        borderRadius={8}
      />
      <div>
        <h4>Props 说明</h4>
        <ul>
          <li>
            <code>triggerSendKey</code> - 触发发送操作的键盘快捷键
          </li>
          <li>
            <code>style</code> - 应用于输入字段的内联样式
          </li>
          <li>
            <code>borderRadius</code> - 边框圆角大小
          </li>
        </ul>
      </div>
    </div>
  );
};

启用提示词优化

import { MarkdownInputField } from '@ant-design/agentic-ui';

export default () => {
  const [value, setValue] = React.useState(
    '请将这段提示语优化为更清晰的英文表达,并保留关键术语。',
  );

  return (
    <div>
      <div
        style={{
          padding: 12,
        }}
      >
        <MarkdownInputField
          value={value}
          onChange={setValue}
          refinePrompt={{
            enable: true,
            onRefine: async (input) => {
              // 模拟异步优化(真实项目可调用后端/模型服务)
              await new Promise((r) => setTimeout(r, 2000));
              return `你好呀,哈哈哈哈 ${input}`;
            },
          }}
        />
      </div>
      <div>
        <h4>说明</h4>
        <ul>
          <li>
            <code>refinePrompt.enable</code> 为 true
            时,右上“快速操作”区域会显示“优化提示词/撤销”按钮
          </li>
          <li>
            <code>refinePrompt.onRefine</code> 接收当前输入文本,返回
            Promise&lt;string&gt; 作为优化后的文本
          </li>
          <li>优化完成后按钮恢复为“优化提示词”;</li>
        </ul>
      </div>
    </div>
  );
};

放大

MarkdownInputField 支持放大功能,用户可以通过点击放大图标将输入框扩展到指定的容器内,提供更大的编辑空间。此功能特别适用于需要编写长文本或复杂内容的场景。

相关属性

属性说明类型默认值版本
enlargeable是否启用放大功能booleanfalse-
enlargeTargetRef放大时的目标容器引用,必须是一个具有定位属性的元素React.RefObject<HTMLElement>--

使用注意事项

  1. 容器定位:目标容器必须具有相对定位(position: relative)或其他非静态定位
  2. 容器尺寸:确保目标容器有足够的空间容纳放大后的输入框
  3. z-index:放大后的输入框具有较高的 z-index 值,确保不被其他元素遮盖
  4. 响应式设计:在移动端或小屏幕设备上使用时,建议调整目标容器尺寸

基本示例

import { MarkdownInputField } from '@ant-design/agentic-ui';

export default () => {
  const [value, setValue] = React.useState(
    '输入文本效果,输入文本效果,输入文本效果,输入文本效果,输入文本效果,输入文本效果,输入文本效果,输入文本效果,输入文本效果,输入文本效果,输入文本效果,输入文本效果',
  );
  const containerRef = React.useRef<HTMLDivElement>(null);

  return (
    <div>
      <div
        style={{
          height: 500,
          padding: 32,
          position: 'relative',
        }}
        ref={containerRef}
      >
        <MarkdownInputField
          value={value}
          onChange={setValue}
          enlargeable={{ enable: true, height: 600 }}
          style={{ marginTop: 128, height: 190 }}
          refinePrompt={{
            enable: true,
            onRefine: async (input) => {
              // 模拟异步优化(真实项目可调用后端/模型服务)
              await new Promise((r) => setTimeout(r, 2000));
              return `你好呀,哈哈哈哈 ${input}`;
            },
          }}
        />
      </div>
    </div>
  );
};

便捷操作区域

便捷操作区提供了在输入框上方自定义操作按钮的功能。

Props 说明:

  • isShowTopOperatingArea - 是否显示便捷操作区域,默认为 false
  • isShowBackTo - 是否显示到顶到底
  • operationBtnRender - 自定义操作按钮渲染函数,用于在便捷操作区中添加自定义按钮
import { Button } from 'antd';
import {
  MarkdownInputField,
  CreateRecognizer,
  type MarkdownEditorInstance,
} from '@ant-design/agentic-ui';

const createRecognizer: CreateRecognizer = async ({ onPartial, onError }) => {
  let timer: ReturnType<typeof setInterval>;
  return {
    start: async () => {
      // 真实场景应启动麦克风与ASR服务,这里仅用计时器模拟持续的转写片段
      let i = 0;
      timer = setInterval(() => {
        onPartial(`语音片段${i} `);
        i += 1;
      }, 500);
    },
    stop: async () => {
      clearInterval(timer);
    },
  };
};
export default () => {
  const [value, setValue] = React.useState(
    [
      '帮我创建一个定时任务。请根据我的描述: `${placeholder:任务名称}` 、 `${placeholder:执行频率}` ,内容如下:',
      '```markdown',
      '任务内容',
      '```',
      '帮我生成合适的定时任务配置。',
    ].join('\n'),
  );

  const markdownRef = React.useRef<MarkdownEditorInstance>(null);
  const targetRef = React.useRef<HTMLDivElement>(null);

  return (
    <div
      style={{
        boxSizing: 'border-box',
        display: 'flex',
        flexDirection: 'column',
        gap: 12,
        height: 450,
        margin: '0 auto',
        maxWidth: 720,
        width: '100%',
      }}
    >
      <div
        style={{
          flex: 1,
          border: '1px solid #e8e8e8',
          borderRadius: '8px',
          overflow: 'auto',
          padding: '16px',
        }}
        ref={targetRef}
      >
        <h1>长内容页面</h1>
        <div
          style={{
            height: '600px',
            background: 'linear-gradient(to bottom, #f0f0f0, #ffffff)',
          }}
        />
        <p>内容结束</p>
      </div>

      <div
        style={{
          borderRadius: '8px',
        }}
      >
        <MarkdownInputField
          value={value}
          targetRef={targetRef}
          inputRef={markdownRef}
          voiceRecognizer={createRecognizer}
          isShowTopOperatingArea={true}
          operationBtnRender={() => (
            <>
              <Button>次按钮</Button>
              <Button type="primary">主按钮</Button>
            </>
          )}
          style={{
            maxHeight: 120,
            maxWidth: '100%',
          }}
          attachment={{
            enable: true,
            accept: '.pdf,.doc,.docx,image/*',
            maxFileSize: 2 * 1024 * 1024, // 2MB,超出将拦截并提示
            maxFileCount: 3,
            upload: async (file) => {
              await new Promise((r) => setTimeout(r, 800));
              return URL.createObjectURL(file);
            },
            onDelete: async () => {},
          }}
          onChange={(newValue) => {
            setValue(newValue);
            console.log('newValue', newValue);
          }}
          placeholder="请输入内容..."
          onSend={async (text) => {
            console.log('发送内容:', text);
            await new Promise((resolve) => setTimeout(resolve, 1000));
          }}
        />
      </div>
    </div>
  );
};

启用附件功能

import { Button } from 'antd';
import { MarkdownInputField } from '@ant-design/agentic-ui';
import { ChevronDown } from '@sofa-design/icons';
export default () => {
  const [value, setValue] = React.useState('');
  return (
    <>
      <MarkdownInputField
        value={value}
        onChange={setValue}
        toolsRender={() => [
          <Button
            type="text"
            key="bold"
            onClick={() => console.log('DeepThink clicked')}
          >
            DeepThink
            <ChevronDown />
          </Button>,
          <Button
            type="text"
            key="italic"
            onClick={() => console.log('深度思考 clicked')}
          >
            深度思考
          </Button>,
          <Button
            type="text"
            key="link"
            onClick={() => console.log('联网搜索 clicked')}
          >
            联网搜索
          </Button>,
        ]}
        attachment={{
          enable: true,
          accept: '.pdf,.doc,.docx,image/*',
          maxFileSize: 10 * 1024 * 1024, // 10MB(字节)
          upload: async (file) => {
            // 模拟上传文件
            await new Promise((resolve) => setTimeout(resolve, 10000));
            return URL.createObjectURL(file);
          },
          onDelete: async (file) => {
            console.log('删除文件:', file);
            await new Promise((resolve) => setTimeout(resolve, 500));
          },
        }}
      />
      <div>
        <h4>Props 说明</h4>
        <ul>
          <li>
            <code>attachment</code> - 附件配置
            <ul>
              <li>
                <code>enable</code> - 是否启用附件功能
              </li>
              <li>
                <code>accept</code> - 接受的文件类型
              </li>
              <li>
                <code>maxFileSize</code> - 文件最大大小限制(字节)
              </li>
              <li>
                <code>upload</code> - 文件上传回调函数
              </li>
              <li>
                <code>onDelete</code> - 文件删除回调函数
              </li>
            </ul>
          </li>
        </ul>
      </div>
    </>
  );
};

最大文件限制与错误展示

通过 maxFileSize(单文件最大字节数)和 maxFileCount(最大文件数量)可限制附件选择。当文件超过大小限制时,该文件会以错误状态出现在附件列表中,并展示「超过 xxx KB」等错误文案;超过数量限制时,多选会被拦截。

文件超过最大值报错

下方 Demo 中单文件限制为 100KB,选择超过该大小的文件时,该文件会以错误状态出现在附件列表并展示「超过 xxx KB」的报错提示。

文件数量超限回调

通过 onExceedMaxCount 回调,可以在文件数量超过 maxFileCount 限制时收到通知,由消费者决定如何展示提示信息,而不是静默失败。

uploadWithResponse - 获取完整上传响应

使用 uploadWithResponse 接口可以返回完整的上传响应对象,包含文件ID、URL、状态等详细信息。响应数据会自动存储在 file.uploadResponse 中。

uploadWithResponse 接口定义

uploadWithResponse?: (
  file: AttachmentFile,
  index: number
) => Promise<UploadResponse>;

UploadResponse 类型

属性说明类型默认值版本
fileId文件ID(必填)string--
fileName文件名(必填)string--
fileType文件类型(必填)string--
fileUrl文件URL(必填)string--
uploadStatus上传状态(必填)'SUCCESS' | 'FAIL' | string--
contentId内容IDstring | null--
errorMessage错误消息string | null--
fileSize文件大小(字节)number | null--

特性

  • ✅ 返回完整的响应对象,包含更多元信息
  • ✅ 响应数据自动存储在 file.uploadResponse 中
  • ✅ 支持自定义错误消息(errorMessage)
  • ✅ 优先级高于旧的 upload 接口
  • ✅ 向后兼容,可与 upload 接口共存

使用示例

<MarkdownInputField
  attachment={{
    enable: true,
    uploadWithResponse: async (file, index) => {
      const response = await api.uploadFile(file);
      return {
        contentId: response.contentId,
        errorMessage: null,
        fileId: response.fileId,
        fileName: file.name,
        fileSize: file.size,
        fileType: file.type,
        fileUrl: response.fileUrl,
        uploadStatus: 'SUCCESS'
      };
    },
    onFileMapChange: (fileMap) => {
      // 访问完整的上传响应数据
      fileMap?.forEach(file => {
        console.log('文件响应:', file);
      });
    }
  }}
/>

自定义附件按钮渲染

通过 attachment.render 属性,您可以完全替换默认的 AttachmentButtonPopover 组件,实现自定义的附件按钮交互体验。

render 属性

属性说明类型默认值版本
render自定义渲染组件函数(props: RenderProps) => React.ReactElement--

RenderProps

属性说明类型默认值版本
children需要包装的子元素,通常是 Paperclip 图标React.ReactNode--
supportedFormat支持的文件格式配置AttachmentButtonPopoverProps['supportedFormat']--

supportedFormat

属性说明类型默认值版本
type文件类型名称string--
extensions支持的文件扩展名数组string[]--
icon文件类型图标React.ReactNode--

兼容性

  • 完全向后兼容,不使用 render 时保持原有行为
  • 支持所有现有的 AttachmentButton 属性
  • 可与其他附件配置选项(如 supportedFormat、maxFileSize 等)配合使用

自定义操作按钮

普通操作按钮可直接使用 Ant Design 的 Button,通过 toolsRender、actionsRender 和 quickActionRender 接入输入框。

import { Button } from 'antd';
import { MarkdownInputField } from '@ant-design/agentic-ui';
import { ChevronDown } from '@sofa-design/icons';
export default () => {
  const [value, setValue] = React.useState('');
  return (
    <>
      <div
        style={{
          padding: 12,
        }}
      >
        <MarkdownInputField
          value={value}
          toolsRender={() => [
            <Button
              type="text"
              key="custom"
              onClick={() => console.log('自定义工具栏按钮')}
            >
              自定义
            </Button>,
            <Button
              type="text"
              key="bold"
              onClick={() => console.log('DeepThink clicked')}
            >
              DeepThink
              <ChevronDown />
            </Button>,
            <Button
              type="text"
              key="italic"
              onClick={() => console.log('深度思考 clicked')}
            >
              深度思考
            </Button>,
            <Button
              type="text"
              key="link"
              onClick={() => console.log('联网搜索 clicked')}
            >
              联网搜索
            </Button>,
          ]}
          onChange={setValue}
          actionsRender={(props) => [
            <button key="custom" onClick={() => console.log('自定义按钮')}>
              自定义
            </button>,
          ]}
          quickActionRender={(props) => [
            <button key="top-right" onClick={() => console.log('右上按钮')}>
              右上
            </button>,
          ]}
        />
      </div>
      <div>
        <h4>Props 说明</h4>
        <ul>
          <li>
            <code>toolsRender</code> - 自定义渲染操作按钮前内容的函数
          </li>
          <li>
            <code>actionsRender</code> - 自定义渲染操作按钮的函数
            <ul>
              <li>
                <code>props</code> - 组件属性
              </li>
              <li>
                <code>defaultActions</code> - 默认的操作按钮数组
              </li>
            </ul>
          </li>
          <li>
            <code>quickActionRender</code> -
            在编辑区域右上、贴右侧渲染按钮组;组件会根据其宽度自动为文本区域预留右侧内边距,避免遮挡。
          </li>
        </ul>
      </div>
    </>
  );
};

获取编辑器实例

import { Button } from 'antd';
import { MarkdownInputField } from '@ant-design/agentic-ui';
import { ChevronDown } from '@sofa-design/icons';

const App = () => {
  const editorRef = React.useRef();
  const [value, setValue] = React.useState('');
  return (
    <>
      <MarkdownInputField
        inputRef={editorRef}
        value={value}
        onChange={setValue}
        toolsRender={() => [
          <Button
            type="text"
            key="bold"
            onClick={() => console.log('DeepThink clicked')}
          >
            DeepThink
            <ChevronDown />
          </Button>,
          <Button
            type="text"
            key="italic"
            onClick={() => console.log('深度思考 clicked')}
          >
            深度思考
          </Button>,
          <Button
            type="text"
            key="link"
            onClick={() => console.log('联网搜索 clicked')}
          >
            联网搜索
          </Button>,
        ]}
      />
      <button
        onClick={() => {
          // 获取编辑器内容
          console.log(editorRef.current?.store?.getMDContent());
          document.getElementById('test').innerHTML =
            editorRef.current?.store?.getMDContent();
        }}
      >
        获取内容
      </button>
      <div id="test" />
    </>
  );
};
export default App;

焦点事件处理

import { Button } from 'antd';
import { MarkdownInputField } from '@ant-design/agentic-ui';
import { ChevronDown } from '@sofa-design/icons';
export default () => {
  const [value, setValue] = React.useState('');
  const [isFocused, setIsFocused] = React.useState(false);

  return (
    <>
      <MarkdownInputField
        value={value}
        onChange={setValue}
        toolsRender={() => [
          <Button
            type="text"
            key="bold"
            onClick={() => console.log('DeepThink clicked')}
          >
            DeepThink
            <ChevronDown />
          </Button>,
          <Button
            type="text"
            key="italic"
            onClick={() => console.log('深度思考 clicked')}
          >
            深度思考
          </Button>,
          <Button
            type="text"
            key="link"
            onClick={() => console.log('联网搜索 clicked')}
          >
            联网搜索
          </Button>,
        ]}
        placeholder="点击输入框获得焦点..."
        onFocus={(value, schema) => {
          console.log('输入框获得焦点:', { value, schema });
          setIsFocused(true);
        }}
        onSend={async (text) => {
          console.log('发送内容:', text);
          await new Promise((resolve) => setTimeout(resolve, 1000));
        }}
      />
      <div style={{ marginTop: 16 }}>
        <p>当前焦点状态: {isFocused ? '已获得焦点' : '未获得焦点'}</p>
      </div>
      <div>
        <h4>Props 说明</h4>
        <ul>
          <li>
            <code>onFocus</code> - 当输入字段获得焦点时触发的回调函数
            <ul>
              <li>
                <code>value</code> - 当前的 markdown 文本值
              </li>
              <li>
                <code>schema</code> - 当前的编辑器 schema
              </li>
            </ul>
          </li>
        </ul>
      </div>
    </>
  );
};

自定义叶子节点渲染

import { MarkdownInputField } from '@ant-design/agentic-ui';
export default () => {
  const [value, setValue] = React.useState('**粗体文本** *斜体文本* `代码`');

  return (
    <MarkdownInputField
      value={value}
      onChange={setValue}
      placeholder="尝试输入 **粗体**、*斜体* 或 `代码`..."
      leafRender={(props, defaultDom) => {
        const { leaf, children } = props;

        // 自定义粗体样式
        if (leaf.bold) {
          return (
            <strong
              style={{
                color: '#1890ff',
                backgroundColor: '#e6f7ff',
                padding: '2px 4px',
                borderRadius: '4px',
              }}
            >
              {children}
            </strong>
          );
        }

        // 自定义斜体样式
        if (leaf.italic) {
          return (
            <em
              style={{
                color: '#722ed1',
                backgroundColor: '#f9f0ff',
                padding: '2px 4px',
                borderRadius: '4px',
              }}
            >
              {children}
            </em>
          );
        }

        // 自定义代码样式
        if (leaf.code) {
          return (
            <code
              style={{
                color: '#d83931',
                backgroundColor: '#fff2f0',
                padding: '2px 6px',
                borderRadius: '6px',
                border: '1px solid #ffccc7',
                fontFamily: 'Monaco, Consolas, monospace',
              }}
            >
              {children}
            </code>
          );
        }

        // 返回默认渲染
        return defaultDom;
      }}
      onSend={async (text) => {
        console.log('发送内容:', text);
        await new Promise((resolve) => setTimeout(resolve, 1000));
      }}
    />
  );
};

技能模式

import { Tag, Button, Space, Switch, Divider } from 'antd';
import { ExperimentOutlined, SettingOutlined } from '@ant-design/icons';
import { MarkdownInputField } from '@ant-design/agentic-ui';

export default () => {
  const [skillModeEnabled, setSkillModeEnabled] = React.useState(true);
  const [enableFeature, setEnableFeature] = React.useState(true);
  const [changeLog, setChangeLog] = React.useState([]);

  return (
    <>
      {/* 控制面板 */}
      <div
        style={{
          marginBottom: 16,
          padding: 16,
          background: '#f6f8fa',
          borderRadius: 6,
        }}
      >
        <Space split={<Divider type="vertical" />}>
          <label>
            功能开关:
            <Switch
              checked={enableFeature}
              onChange={setEnableFeature}
              style={{ marginLeft: 8 }}
            />
          </label>
          <label>
            显示控制:
            <Switch
              checked={skillModeEnabled}
              onChange={setSkillModeEnabled}
              disabled={!enableFeature}
              style={{ marginLeft: 8 }}
            />
          </label>
          <Button size="small" onClick={() => setChangeLog([])}>
            清空日志
          </Button>
        </Space>

        <div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
          <p style={{ margin: '4px 0' }}>
            <strong>enable={enableFeature ? 'true' : 'false'}</strong> -{' '}
            {enableFeature
              ? '功能启用时,组件正常渲染和工作'
              : '功能禁用时,组件完全不渲染,不执行任何逻辑'}
          </p>
          <p style={{ margin: '4px 0' }}>
            <strong>open={skillModeEnabled ? 'true' : 'false'}</strong> -{' '}
            控制技能模式的显示与隐藏
          </p>
        </div>
      </div>

      <MarkdownInputField
        placeholder="请输入内容..."
        skillMode={{
          enable: enableFeature, // 控制整个功能是否启用
          open: skillModeEnabled,
          title: (
            <Space>
              <ExperimentOutlined />
              AI助手模式
            </Space>
          ),
          rightContent: [
            <Tag key="version" color="blue">
              v2.0
            </Tag>,
            <Tag
              key="status"
              color={enableFeature ? 'green' : 'red'}
              style={{ fontSize: 11 }}
            >
              {enableFeature ? '已启用' : '已禁用'}
            </Tag>,
            <Button
              key="settings"
              type="text"
              size="small"
              icon={<SettingOutlined />}
              onClick={() => console.log('设置点击')}
            >
              设置
            </Button>,
          ],
          closable: true,
        }}
        onSkillModeOpenChange={(open) => {
          const timestamp = new Date().toLocaleTimeString();
          const actionText = open ? '打开' : '关闭';
          const logEntry = `[${timestamp}] ${actionText}`;

          setChangeLog((prev) => [logEntry, ...prev.slice(0, 4)]);
          setSkillModeEnabled(open);
        }}
        onSend={async (text) => {
          console.log('发送内容:', text);
        }}
      />

      {/* 状态变化日志 */}
      {changeLog.length > 0 && (
        <div
          style={{
            marginTop: 16,
            padding: 12,
            background: '#f6f8fa',
            borderRadius: 6,
            maxHeight: 120,
            overflow: 'auto',
          }}
        >
          <strong>状态变化日志:</strong>
          {changeLog.map((log, index) => (
            <div
              key={index}
              style={{ fontSize: 12, color: '#666', margin: '4px 0' }}
            >
              {log}
            </div>
          ))}
        </div>
      )}

      <div style={{ marginTop: 16 }}>
        <h4>Props 说明</h4>
        <ul>
          <li>
            <code>skillMode</code> - 技能模式配置
            <ul>
              <li>
                <code>enable</code> - 是否启用技能模式组件,默认为 true
              </li>
              <li>
                <code>open</code> - 是否打开技能模式
              </li>
              <li>
                <code>title</code> - 技能模式标题,支持React节点
              </li>
              <li>
                <code>rightContent</code> - 右侧自定义内容数组
              </li>
              <li>
                <code>closable</code> - 是否显示默认关闭按钮
              </li>
              <li>
                <code>style</code> - 容器样式
              </li>
              <li>
                <code>className</code> - 容器类名
              </li>
            </ul>
          </li>
          <li>
            <code>onSkillModeOpenChange</code> -
            技能模式状态变化时触发的回调函数
            <ul>
              <li>
                <code>open</code> - 新的开关状态
              </li>
            </ul>
          </li>
        </ul>
      </div>
    </>
  );
};

内联原子 Chip(slash 命令 / @file / @symbol)

开启 composerChips.enable 后,编辑器支持 composer-chip 内联原子节点(对齐桌面 IDE 聊天输入框的 slash 命令与 mention 体验):

  • slash 命令 chip:/name 结构化技能节点。chip 整体删除(Backspace),点击触发 onSlashChipClick 打开宿主配置浮层;未配置参数(无 payload)的 chip 在发送前被 gate 拦截(onSlashChipGate)。
  • @file / @folder chip:文件 / 目录引用,发送时序列化为 @path(路径含空格自动加尖括号 @<path>)。
  • @symbol chip:符号引用(函数 / 类),展示 symbolKind 副标题与容器名。
import {
  MarkdownInputField,
  createFileChipNode,
  createSlashChipNode,
  createSymbolChipNode,
  insertComposerChip,
} from '@ant-design/agentic-ui';

// 宿主侧插入 chip(如 slash 命令面板选中、@补全选中)
insertComposerChip(editor, {
  kind: 'slash',
  name: 'plan',
  section: 'skill',
  extensionId: 'my-ext',
});
insertComposerChip(editor, {
  kind: 'file',
  path: 'src/MarkdownInputField/index.tsx',
  name: 'index.tsx',
});
insertComposerChip(editor, {
  kind: 'symbol',
  name: 'useComposerDraft',
  symbolKind: 'function',
  containerName: 'hooks',
});

发送前 gate(存在未配置 slash chip 时阻止发送并提示):

<MarkdownInputField
  composerChips={{
    enable: true,
    onSlashChipClick: (chip) => openSkillConfigOverlay(chip),
    onSlashChipGate: (chips) => {
      message.warning(`请先配置 /${chips[0].name} 的参数`);
      return true; // 阻止发送;返回 false 放行
    },
  }}
/>

长文本粘贴折叠

开启 longTextFold.enable 后,纯文本粘贴超过阈值(≥ 1200 字符或 ≥ 12 行)时自动折叠为单个「长文本」chip,展示字符数与行数统计,避免一次粘贴撑爆输入框;发送时自动还原完整原文(多行原文以 fenced code 形式序列化)。

点击长文本 chip 可弹出原文预览浮层(对齐 IDE ComposerLongTextPreview),在不展开编辑器的情况下核对折叠内容。

<MarkdownInputField longTextFold={{ enable: true }} />

草稿恢复

传入 draft.draftKey 后自动持久化输入内容(fragment 级,对齐 IDE 会话切换体验):

  • 提交时机:输入停顿(idle,默认 180ms)、失焦(blur)、发送成功(send)、draftKey 切换(switch)、组件卸载(unmount);
  • 恢复时机:draftKey 变化且当前编辑器为空时自动回填;
  • 默认写入 localStorage(key 为 draftKey 派生),可通过 storage 替换为内存 / IndexedDB 等自定义存储。
<MarkdownInputField
  draft={{
    draftKey: sessionId,
    idleDelay: 200,
    onDraftCommit: (value, reason) => {
      // 返回 false 表示宿主自行持久化,hook 跳过默认写入
      return persistDraft(sessionId, value);
    },
  }}
/>

上下文用量指示

配置 contextUsage 后在工具栏渲染环形用量指示器:16px 圆环 + 分档颜色(<50% 灰、≥50% 黄、≥80% 高用量深黄、溢出红),悬浮展示 used / window (percent%)。

内置面板:未传 onClick 且提供 categories 时,点击指示器展开内置 ContextUsagePanel——占用百分比 + token 摘要、分类分段条形图(分类合计超出/不足总占用时自动缩放/补残差)、分类明细行(可展开 items)、高用量警告行、compact 压缩操作(onCompact / compacting / compactDisabled)。传入 onClick 则由宿主自管面板。

<MarkdownInputField
  contextUsage={{
    usedTokens: 42000,
    contextWindow: 128000,
    highUsageThreshold: 80,
    categories: [
      { key: 'system', name: '系统', tokenCount: 12000 },
      {
        key: 'skills',
        name: '技能',
        tokenCount: 8000,
        items: [{ label: 'pdf-reader', estimatedTokens: 5000 }],
      },
      { key: 'messages', name: '会话', tokenCount: 22000 },
    ],
    onCompact: () => compactSession(),
    compacting: false,
  }}
/>

也可直接使用独立导出的 ContextUsageIndicator / ContextUsagePanel 组件与 computeBarSegments 工具函数自定义布局。

分支选择

配置 branch 后在工具栏渲染分支触发器:分支图标 + 当前分支名 pill,点击展开可搜索的分支菜单。不传 branches 时仅展示当前分支不可切换。

菜单能力(对齐 IDE ComposerBranchMenuView):

  • 搜索过滤(本地过滤或 onSearch 远程);
  • 当前分支概览:currentBranchOverview 展示未提交文件数与 +新增/-删除 行数;
  • 逐分支切换 spinner:switching 传分支名时仅对该分支行显示 loading(全局切换传 true);
  • 新建分支入口:传入 onCreateBranch 时显示;
  • 文案定制:labels 覆盖搜索占位、分组标题、remote 标记等。
<MarkdownInputField
  branch={{
    branchName: 'feat/composer',
    tooltipTitle: '切换工作分支',
    branches: [
      { name: 'main', displayName: 'main', isRemote: false, isCurrent: false },
      {
        name: 'feat/composer',
        displayName: 'composer',
        isRemote: false,
        isCurrent: true,
      },
      {
        name: 'origin/main',
        displayName: 'origin/main',
        isRemote: true,
        isCurrent: false,
      },
    ],
    currentBranchOverview: { fileCount: 3, insertions: 120, deletions: 8 },
    switching: false,
    onSelectBranch: (name) => switchBranch(name),
    onCreateBranch: () => openCreateDialog(),
  }}
/>

也可直接使用独立导出的 ComposerBranchTrigger 组件。

粘贴配置

import { MarkdownInputField } from '@ant-design/agentic-ui';

export default () => {
  const [value, setValue] = React.useState('');

  return (
    <>
      <MarkdownInputField
        value={value}
        onChange={setValue}
        placeholder="只能粘贴纯文本内容..."
        pasteConfig={{
          enabled: true,
          allowedTypes: ['text/plain'],
        }}
        onSend={async (text) => {
          console.log('发送内容:', text);
          await new Promise((resolve) => setTimeout(resolve, 1000));
        }}
      />
      <div style={{ marginTop: 16 }}>
        <h4>Props 说明</h4>
        <ul>
          <li>
            <code>pasteConfig</code> - 粘贴配置
            <ul>
              <li>
                <code>enabled</code> - 是否启用粘贴功能,默认为 true
              </li>
              <li>
                <code>allowedTypes</code> - 允许的粘贴内容类型
                <ul>
                  <li>
                    <code>application/x-slate-md-fragment</code> - Slate
                    Markdown 片段
                  </li>
                  <li>
                    <code>text/html</code> - HTML 内容
                  </li>
                  <li>
                    <code>Files</code> - 文件
                  </li>
                  <li>
                    <code>text/markdown</code> - Markdown 文本
                  </li>
                  <li>
                    <code>text/plain</code> - 纯文本
                  </li>
                </ul>
              </li>
            </ul>
          </li>
        </ul>
      </div>
    </>
  );
};