@bdky/agent-chat-ui
v0.2.0
Published
AI 对话界面 UI 组件库,基于 Tiptap 富文本编辑器,提供开箱即用的 Agent 聊天 React 组件
Readme
@bdky/agent-chat-ui
English | 简体中文
基于 TipTap 的 AI Agent 聊天输入框:行为由库负责,渲染完全由你决定。
@bdky/agent-chat-ui 基于 TipTap(ProseMirror)构建,提供 / 技能、@ 上下文等触发字符、行内标签、模板插槽、字数统计与提交控制。触发、键盘导航、选中插入、提交与超限拦截、无障碍语义都由库保证一致;输入框外壳、下拉面板、编辑器内标签的外观全部可以替换。
✨ 特性
- 🧩 部件化定制 —
AgentInput.*/MentionMenu.*部件无样式,按钮类支持asChild,状态以data-*属性暴露 - 🔤 多触发字符 —
/、@等任意触发字符,同一触发字符下可区分技能、指令、文件等kind - 🏷️ 行内标签 — 选中项作为不可编辑的标签内联插入,按 kind 自定义渲染与序列化
- ⌨️ 键盘与无障碍 — ↑↓ / Enter / Esc 导航,listbox / option 语义,输入法组合态安全
- 🔢 字数与超限 — 拦截输入或允许超出并禁止提交,字数可与提交文本长度一致
- 📝 模板插槽 —
{{variable}}占位符在编辑器中变为行内可编辑的输入框 - 🎨 CSS 变量主题 —
--acu-*token 运行时可覆盖,默认皮肤按实例开启 - 📘 完整 TypeScript 支持 — 开箱即用的类型定义
📦 安装
npm install @bdky/agent-chat-ui
# 或
yarn add @bdky/agent-chat-ui
# 或
pnpm add @bdky/agent-chat-ui对等依赖: react、react-dom(^17.0.0 || ^18.0.0 || ^19.0.0)
入口路径:
| 导入路径 | 内容 |
|----------|------|
| @bdky/agent-chat-ui | 全部导出(核心 + React) |
| @bdky/agent-chat-ui/core | 框架无关的核心层(TipTap 扩展、工具函数、类型) |
| @bdky/agent-chat-ui/react | React hooks、组件、部件 |
| @bdky/agent-chat-ui/theme | 副作用导入,等同于 theme/default.css |
| @bdky/agent-chat-ui/theme/default.css | token + 预设组件样式 + 部件默认皮肤 |
| @bdky/agent-chat-ui/theme/input.css | token + 预设组件样式(acu-input__*) |
| @bdky/agent-chat-ui/theme/parts.css | token + 部件默认皮肤(skin="default" 时生效) |
| @bdky/agent-chat-ui/theme/tokens.css | 仅 CSS 变量 |
🚀 快速开始
有两种用法:部件(完全定制外观)和预设组件(固定布局,快速接入)。
部件
import {AgentInput, MentionMenu, useAgentInput} from '@bdky/agent-chat-ui';
import '@bdky/agent-chat-ui/theme';
const ChatInput = () => {
const input = useAgentInput({
placeholder: '输入 @ 选择员工',
onSubmit: content => send(content),
triggers: [
{char: '@', items: ({query}) => searchAgents(query)}
]
});
return (
<AgentInput.Root input={input} skin="default">
<AgentInput.Editor maxHeight={200} />
<AgentInput.Footer>
<AgentInput.TriggerButton char="@">@ 提及</AgentInput.TriggerButton>
<AgentInput.Counter />
<AgentInput.Submit />
</AgentInput.Footer>
<MentionMenu.Root trigger="@">
<MentionMenu.Header>选择员工</MentionMenu.Header>
<MentionMenu.Empty>没有匹配的员工</MentionMenu.Empty>
<MentionMenu.List>
{({items}) => items.map(item => (
<MentionMenu.Item key={item.id} value={item}>{item.label}</MentionMenu.Item>
))}
</MentionMenu.List>
</MentionMenu.Root>
</AgentInput.Root>
);
};skin="default" 启用库自带皮肤;去掉它,部件就完全没有样式,由你自己写(见下文「主题」)。
预设组件
import {useAgentInput, AgentInput} from '@bdky/agent-chat-ui';
import '@bdky/agent-chat-ui/theme';
const ChatInput = () => {
const {editor, isEmpty, isFocused, submit, clear} = useAgentInput({
placeholder: '请输入消息...',
onSubmit: content => {
send(content);
clear();
}
});
return (
<AgentInput
editor={editor}
isEmpty={isEmpty}
isFocused={isFocused}
onSubmit={submit}
className={`acu-input${isFocused ? ' acu-input--focused' : ''}`}
/>
);
};🏗️ 架构
@bdky/agent-chat-ui
├── core/ ← 框架无关
│ └── input/
│ ├── extensions/ InputSlot · SubmitShortcut · AgentMention · CharacterLimit
│ ├── mentionMenuStore 下拉面板状态与键盘导航
│ ├── utils docToPlainText · extractMentions · buildTemplateHTML …
│ └── types
├── react/
│ └── input/
│ ├── useAgentInput 主 hook
│ ├── AgentInput 预设组件 + AgentInput.* 部件
│ ├── MentionMenu MentionMenu.* 部件
│ ├── MentionChip 编辑器内标签的默认渲染
│ └── MentionPanel · MentionNodeView · InputSlotView
└── theme/ tokens · input(预设) · parts(部件默认皮肤) · default(全部)| 层 | 内容 | 由谁决定 |
|---|---|---|
| 行为 | useAgentInput 持有编辑器、提交、字数、触发与面板状态 | 库 |
| 结构 | AgentInput.*、MentionMenu.* 部件与节点渲染器 | 库定义语义,你给内容 |
| 样式 | data-* 状态属性、--acu-* CSS 变量、可选默认皮肤 | 你 |
完整设计见 docs/customization-design.md。
🔤 Mention(触发字符)
const input = useAgentInput({
onSubmit: send,
triggers: [
{
char: '/',
items: ({query}) => searchSkillsAndCommands(query),
onBeforeSelect: ({item, mentions}) => {
const duplicated = mentions.some(m => m.kind === 'skill' && m.id === item.id);
if (duplicated) {
toast('该技能已添加');
}
return !duplicated;
}
},
{
char: '@',
items: ({query}) => searchFiles(query),
toText: ({id}) => `@${id}`
}
]
});MentionTrigger:
| 字段 | 类型 | 说明 |
|------|------|------|
| char | string | 触发字符 |
| items | ({query}) => MentionMenuItem[] \| Promise<…> | 查询候选;返回顺序即候选数据顺序 |
| onBeforeSelect | ({item, mentions, editor}) => boolean | 选中或 insertMention 前的业务校验;返回 false 时取消插入,并清掉已输入的触发文本 |
| toText | ({id, label, kind}) => string | 该触发字符的标签如何序列化;不传为「触发字符 + label」 |
| deleteTriggerWithBackspace | boolean | 退格删除该触发字符的标签时是否连触发字符一起删,默认 false |
MentionMenuItem: {id, label, kind?, disabled?, ...任意业务字段}。kind 会写入插入后的标签,用于区分同一触发字符下的不同类别;disabled 的项不可高亮、不可选中。
规则:
- 触发字符前须为行首、空格或另一个标签,所以
src/utils、[email protected]这类文本不会误弹面板。 triggerMention(char)与AgentInput.TriggerButton在光标紧跟文字时会先补一个空格,保证面板能弹出;手打时不补。- 触发字符集合在编辑器创建时确定;
items、onBeforeSelect、toText每次渲染传入的新函数即时生效。 - 退格删除标签时默认退回成触发字符并重新唤起面板;该触发字符设
deleteTriggerWithBackspace: true时一次删干净。 triggers与extensions里的AgentMention只能二选一,同时传入会抛错。需要任意位置触发时,改用extensions: [AgentMention.configure(...)](见 AgentMention)。
读取与插入:
input.getContent();
// "帮我用 /xlsx 整理 @docs/report.md"
input.getMentions();
// [
// {id: 'xlsx', label: 'xlsx', char: '/', kind: 'skill'},
// {id: 'docs/report.md', label: 'report.md', char: '@', kind: 'file'}
// ]
// 附件、技能按钮等非键入的插入:同样走 onBeforeSelect
input.insertMention({char: '@', id: file.path, label: file.name, kind: 'file'});🔢 字数与超限
const input = useAgentInput({
maxLength: 1000,
overflow: 'allow',
countLineBreaks: true,
onSubmit: send
});| 选项 | 说明 |
|------|------|
| maxLength | 最大字符数,含标签的序列化文本 |
| overflow: 'block'(默认) | 超出时拦截输入 |
| overflow: 'allow' | 允许超出;超出时回车与 submit() 都不提交,isOverLimit 为真 |
| countLineBreaks | 为 true 时段落换行计入字数,charCount 与 getContent().length 一致,可预测提交长度 |
charCount、isOverLimit、canSubmit(非空且未超限)供渲染使用;AgentInput.Counter 与 AgentInput.Submit 已接好。
🧩 部件
所有部件都放在 AgentInput.Root 内,接受 className、style 与对应元素的原生属性。
AgentInput.*
| 部件 | 行为 | 默认元素 | 状态属性 |
|------|------|----------|----------|
| Root | 提供上下文;点击空白处聚焦编辑区;renderMention 自定义标签;skin="default" 开启默认皮肤 | div.acu-input-root | data-focused data-empty data-over-limit data-acu-skin |
| Header / Footer | 无 | div.acu-input-header / div.acu-input-footer | — |
| Editor | 编辑区;maxHeight / minHeight(px) | div.acu-input-editor | — |
| TriggerButton | 插入触发字符(char,默认 @)并唤起面板 | button.acu-input-trigger | — |
| Counter | 显示字数;children 可为 ({count, limit, over}) => ReactNode | span.acu-input-counter,默认 count/limit | data-over-limit |
| Submit | 调用 submit(),!canSubmit 时禁用 | button.acu-input-submit,默认为发送图标 | data-disabled |
asChild: TriggerButton、Submit 设 asChild 后不渲染自带元素,而是把行为合并到唯一子元素上:子元素的 onClick 先执行,className 拼接,disabled、data-*、aria-* 以部件为准,默认 class 不再输出。
<AgentInput.Submit asChild>
<MyButton icon="send" />
</AgentInput.Submit>MentionMenu.*
| 部件 | 行为 | 状态属性 |
|------|------|----------|
| Root | 仅在 trigger 激活时渲染;portal 到 body;role="listbox";无候选且没有 Empty 时隐藏 | data-state="open" data-trigger data-empty data-acu-skin |
| Header | children 或 ({query, trigger}) => ReactNode | — |
| List | children 或 ({items, query}) => ReactNode | — |
| Group | 语义分组(role="group"),heading 为任意节点 | — |
| Item | value 为候选项;悬停高亮、点击选中、高亮时滚动进可视区;role="option" | data-highlighted data-disabled |
| Empty | 候选为空时渲染 | — |
Root 的定位参数:anchor('input' 以 AgentInput.Root 为锚点并同宽,默认;'caret' 跟随光标)、placement(默认 'bottom-start')、offset(默认 4)、matchWidth。定位结果以 CSS 变量写在面板上:--acu-menu-available-height、--acu-menu-anchor-width。
键盘(库负责):
- ↑ ↓ 按已渲染
Item的 DOM 顺序循环,跳过禁用项;分组、标题等结构不影响导航。 - Enter 选中高亮项(默认高亮第一项);Esc 关闭面板。
- 没有候选时不接管按键:Enter 照常提交或换行。
- 输入法组合态中的按键不触发导航与提交。
- 面板打开时编辑区带
aria-expanded、aria-controls,并以aria-activedescendant指向高亮项。
编辑器内标签:renderMention
<AgentInput.Root
input={input}
renderMention={({label, char, kind, deleteNode}) => (
kind === 'file'
? <FileChip name={label} onRemove={deleteNode} />
: <SkillChip char={char} name={label} />
)}
>参数为 {id, label, char, kind, deleteNode}。库输出的外层只有 span.acu-mention[data-kind],不带视觉样式;不传 renderMention 时用 MentionChip(span.acu-mention-chip,样式来自默认皮肤)。
⚛️ React API
useAgentInput(options?)
参数:
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| placeholder | string | '请输入...' | 占位文本,运行时可切换 |
| submitKey | 'Enter' \| 'Ctrl+Enter' \| 'Meta+Enter' | 'Enter' | 提交快捷键 |
| onSubmit | (content: string) => void | - | 提交回调;不传时提交键不被接管(Enter 换行);是否传入在编辑器创建时确定 |
| onFocus / onBlur | () => void | - | 聚焦 / 失焦回调 |
| triggers | MentionTrigger[] | - | 触发字符配置,见上文「Mention」 |
| maxLength | number \| null | null | 最大字符数 |
| overflow | 'block' \| 'allow' | 'block' | 超限处理 |
| countLineBreaks | boolean | false | 字数是否计入段落换行 |
| extensions | AnyExtension[] | [] | 额外的 TipTap 扩展 |
编辑区高度在 AgentInput(maxHeight / minHeight)或 AgentInput.Editor 上设置。
返回值 UseAgentInputReturn:
| 属性 | 类型 | 说明 |
|------|------|------|
| editor | Editor \| null | TipTap 编辑器实例 |
| isEmpty / isFocused | boolean | 是否为空 / 是否聚焦 |
| charCount | number | 当前字数(含标签序列化文本) |
| maxLength | number \| null | 最大字符数 |
| isOverLimit | boolean | overflow: 'allow' 时是否超限 |
| canSubmit | boolean | 非空且未超限 |
| submit() | () => void | 提交;序列化为空或超限时不提交 |
| clear() | () => void | 清空内容 |
| getContent() | () => string | 提交用纯文本,与回车、按钮提交口径一致 |
| getMentions() | () => MentionValue[] | 所有标签 {id, label, char, kind} |
| insertMention(attrs) | (attrs: InsertMentionAttrs) => boolean | 在光标处插入标签并补空格;返回是否插入 |
| triggerMention(char?) | (char?: string) => void | 插入触发字符(默认 @)以唤起面板 |
| setTemplate(template, slots) | (template: string, slots: SlotConfig[]) => void | 用带插槽的模板设置内容 |
| getSlotValues() | () => Record<string, string> | 所有插槽值 |
| mentionMenuStore | MentionMenuStore | 面板状态,供 MentionMenu 部件使用 |
AgentInput(预设组件)
固定布局:[文件区] + [编辑区] + [工具栏 + 发送键],使用 theme/input.css 的 acu-input__* 样式。
| 属性 | 类型 | 说明 |
|------|------|------|
| editor | Editor \| null | 来自 useAgentInput |
| isEmpty / isFocused | boolean | 来自 useAgentInput |
| onSubmit | () => void | 通常传 submit |
| className / style | - | 根节点,通常 className="acu-input",聚焦时加 acu-input--focused |
| maxHeight / minHeight | number | 编辑区高度(px) |
| renderToolbar | (ctx: AgentInputToolbarContext) => ReactNode | 工具栏左侧内容;ctx.triggerMention(char?) 可唤起面板 |
| renderFileArea | (ctx: AgentInputSlotContext) => ReactNode | 编辑区上方的文件区 |
| renderSendButton | (ctx: AgentInputSlotContext) => ReactNode | 替换发送键 |
| renderSlot | (editorContent, ctx) => ReactNode | 完全自定义内部布局,存在时忽略上面三项 |
AgentInputSlotContext 为 {editor, submit, clear, isEmpty, isFocused};AgentInputToolbarContext 另含 triggerMention(char?)。
AgentInputContext / useAgentInputContext()
AgentInput 与 AgentInput.Root 都会提供该上下文,后代组件可读取 {editor, submit, clear, isEmpty, isFocused}。
const SendButton = () => {
const {submit, isEmpty} = useAgentInputContext();
return <button disabled={isEmpty} onClick={submit}>发送</button>;
};MentionPanel / MentionNodeView
直接配置 AgentMention 扩展(suggestion.render + ReactRenderer)时使用的面板与标签视图,参见 Storybook Input/Mentions。
MentionPanel:接收 suggestion props,另有renderItem(item, {active, select})、children({items, selectedIndex, command})、renderEmpty({query})、title、anchorEl;通过 ref 的onKeyDown接管键盘。MentionNodeView:renderTag({id, label, char, kind})自定义标签内容。
InputSlotView
InputSlot 节点的 React 视图,由 useAgentInput 自动配置。处理输入法组合事件,阻止 Enter 冒泡,按内容自动调整宽度。
🧱 核心层
TipTap 扩展
InputSlot
行内、原子化、可编辑的插槽节点,序列化为其 value。
| 属性 | 类型 | 说明 |
|------|------|------|
| id | string | 插槽唯一标识 |
| placeholder | string | 为空时的占位文本 |
| value | string | 当前值 |
editor.commands.insertSlot({id: 'username', placeholder: '请输入名称', value: ''});
editor.commands.updateSlotValue('username', 'Alice');从 <input-slot id placeholder value> 标签解析与渲染。Backspace 先选中插槽再删除,←→ 跳过插槽。
SubmitShortcut
| 选项 | 类型 | 说明 |
|------|------|------|
| submitKey | 'Enter' \| 'Ctrl+Enter' \| 'Meta+Enter' | 提交快捷键 |
| onSubmit | (content: string) => void | 不传时不接管提交键 |
AgentMention
基于 @tiptap/extension-mention 的标签节点(节点名 agentMention),属性 id、label、mentionSuggestionChar(触发字符)、kind。
| 选项 | 说明 |
|------|------|
| suggestion | 单个触发字符的 suggestion 配置,默认 {char: '@', allowSpaces: false} |
| suggestions | 多个触发字符,如 [{char: '@', …}, {char: '/', …}];传入后忽略 suggestion |
| renderText | 自定义序列化文本,可按 node.attrs.kind 分支 |
| deleteTriggerWithBackspace | 退格删除标签时是否连触发字符一起删,默认 false |
| HTMLAttributes | 标签节点的 HTML 属性 |
allowedPrefixes 默认 [' '](触发字符前须为行首 / 空格 / 标签),需要任意位置触发时设为 null。
CharacterLimit
| 选项 | 说明 |
|------|------|
| limit | 最大字符数,超出时拦截输入;null 不限制 |
| countLineBreaks | 是否计入段落换行 |
通过 editor.storage.characterLimit.characters() 读取当前字数。
工具函数
| 函数 | 说明 |
|------|------|
| docToPlainText(doc) | 按各节点 renderText 提取纯文本(段落以 \n 分隔,去首尾空白);回车提交、submit()、getContent() 同一口径 |
| extractMentions(doc) | 提取所有标签 MentionValue[] |
| extractSlotValues(doc) | 提取插槽值 Record<id, value> |
| buildTemplateHTML(template, slots) | 把含 {{id}} 的模板转为带 <input-slot> 的 HTML |
| sanitizeText(text) | 移除零宽字符,把不间断空格替换为普通空格 |
| createMentionMenuStore(getTriggers) | 下拉面板 store(useAgentInput 内部使用,也可用于其他框架适配) |
粘贴多段文本会被合并为一行(已知行为)。
📝 模板插槽
{{id}} 形式的占位符会在编辑器中变为可编辑的输入框。
const {setTemplate, getSlotValues, getContent} = useAgentInput({onSubmit: send});
setTemplate(
'你好 {{name}},请帮我处理 {{topic}}',
[
{id: 'name', placeholder: '您的名字', defaultValue: 'World'},
{id: 'topic', placeholder: '描述您的主题'}
]
);
getSlotValues();
// {name: 'Alice', topic: 'TypeScript 泛型'}
getContent();
// "你好 Alice,请帮我处理 TypeScript 泛型"🎨 主题
CSS 变量
所有 token 以 --acu-* 定义在 :root,可在任意祖先节点覆盖,运行时生效:
.my-chat {
--acu-input-border-focus: #6c47ff;
--acu-menu-item-active-bg: #f4f1ff;
}| 变量 | 默认值 | 用途 |
|------|--------|------|
| --acu-input-bg | #fff | 输入框背景 |
| --acu-input-border | #e0e0e0 | 边框 |
| --acu-input-border-focus | #3366ff | 聚焦边框、光标颜色 |
| --acu-input-border-hover | #6690ff | 悬停边框 |
| --acu-input-placeholder-color | #bfbfbf | 占位文本 |
| --acu-input-text-color | #262626 | 正文文字 |
| --acu-input-disabled-bg | #f5f5f5 | 禁用背景(预设组件) |
| --acu-input-disabled-color | #bfbfbf | 禁用文字(预设组件) |
| --acu-input-radius | 16px | 圆角 |
| --acu-input-font-size | 14px | 字号 |
| --acu-input-line-height | 1.6 | 行高 |
| --acu-input-min-height | 40px | 编辑区最小高度 |
| --acu-input-padding-x | 16px | 编辑区水平内边距 |
| --acu-input-padding-y | 12px | 编辑区垂直内边距 |
| --acu-input-slot-bg | #f0f5ff | 插槽背景 |
| --acu-input-slot-border | #adc6ff | 插槽边框 |
| --acu-input-slot-radius | 4px | 插槽圆角 |
| --acu-input-slot-padding-x | 4px | 插槽水平内边距 |
| --acu-input-slot-padding-y | 1px | 插槽垂直内边距 |
| --acu-mention-tag-bg | #f0f5ff | 标签背景 |
| --acu-mention-tag-border | #d6e4ff | 标签边框 |
| --acu-mention-tag-color | #3366ff | 标签文字 |
| --acu-mention-tag-radius | 12px | 标签圆角 |
| --acu-file-area-max-height | 190px | 文件区 / Header 最大高度 |
| --acu-file-area-border | #f0f0f0 | 文件区分隔线(预设组件) |
| --acu-toolbar-padding-x | 12px | 工具栏 / Footer 水平内边距 |
| --acu-toolbar-padding-y | 6px | 工具栏 / Footer 垂直内边距 |
| --acu-toolbar-btn-color | #666 | 工具栏按钮文字 |
| --acu-toolbar-btn-hover-bg | #f0f0f0 | 工具栏按钮悬停背景 |
| --acu-toolbar-btn-hover-color | #333 | 工具栏按钮悬停文字 |
| --acu-toolbar-btn-radius | 6px | 工具栏按钮圆角 |
| --acu-toolbar-divider-color | #e5e5e5 | 工具栏分隔线(预设组件) |
| --acu-send-btn-size | 36px | 发送键尺寸 |
| --acu-send-btn-bg | #1a1a1a | 发送键背景 |
| --acu-send-btn-hover-bg | #333 | 发送键悬停背景 |
| --acu-send-btn-disabled-bg | #e0e0e0 | 发送键禁用背景 |
| --acu-send-btn-disabled-color | #bbb | 发送键禁用文字 |
| --acu-btn-primary-bg | #1677ff | 操作区主按钮背景(预设组件) |
| --acu-btn-primary-hover-bg | #4096ff | 操作区主按钮悬停背景(预设组件) |
| --acu-btn-primary-color | #fff | 操作区主按钮文字(预设组件) |
| --acu-btn-secondary-bg | #f5f5f5 | 次按钮背景(预设组件) |
| --acu-btn-secondary-hover-bg | #e8e8e8 | 次按钮悬停背景(预设组件) |
| --acu-btn-secondary-color | #262626 | 次按钮文字(预设组件) |
| --acu-btn-secondary-border | #d9d9d9 | 次按钮边框(预设组件) |
| --acu-btn-radius | 6px | 操作区按钮圆角(预设组件) |
| --acu-btn-padding-x | 16px | 操作区按钮水平内边距(预设组件) |
| --acu-btn-padding-y | 6px | 操作区按钮垂直内边距(预设组件) |
| --acu-btn-font-size | 14px | 操作区按钮字号(预设组件) |
| --acu-input-transition-duration | 0.2s | 过渡时长 |
| --acu-color-primary | #3366ff | 主色(部件光标颜色) |
| --acu-color-danger | #ff4d4f | 超限警示色 |
| --acu-color-text-secondary | #8c8c8c | 次要文字(计数器、面板标题) |
| --acu-menu-bg | #fff | 面板背景 |
| --acu-menu-border | #e5e5e5 | 面板边框 |
| --acu-menu-radius | 12px | 面板圆角 |
| --acu-menu-shadow | 0 8px 32px rgba(0, 0, 0, 0.1) | 面板阴影 |
| --acu-menu-max-height | 280px | 面板最大高度 |
| --acu-menu-item-active-bg | #f5f8ff | 高亮项背景 |
默认皮肤
引入 @bdky/agent-chat-ui/theme(或 theme/default.css),并在 AgentInput.Root 上设 skin="default",面板自动继承。皮肤规则只作用于带 data-acu-skin="default" 的实例,同一页面里自定义皮肤的实例不受影响;未开启时部件没有任何样式。
预设组件 AgentInput 使用 acu-input__* 样式,不需要 skin 属性:
| 类名 | 说明 |
|------|------|
| .acu-input | 根容器(由使用方加在 className 上) |
| .acu-input--focused / .acu-input--disabled | 聚焦 / 禁用(由使用方切换) |
| .acu-input__file-area / __editor / __toolbar | 文件区 / 编辑区 / 工具栏 |
| .acu-input__toolbar-btn / __toolbar-divider | 工具栏按钮 / 分隔线 |
| .acu-input__send-btn / __send-btn--disabled | 发送键 |
| .acu-input__slot / __mention-tag | 插槽 / 标签 |
自定义皮肤
部件不带样式,直接面向稳定 class 与 data-* 状态属性写 CSS。状态属性为「存在即为真」,状态为假时不输出:
.my-input { border: 1px solid #ddd; border-radius: 12px; }
.my-input[data-focused] { border-color: #6c47ff; }
.my-input[data-over-limit] .acu-input-counter { color: #ff4d4f; }
.my-menu .acu-menu-item[data-highlighted] { background: #f4f1ff; }
.my-input .acu-mention[data-kind='file'] { opacity: .9; }稳定 class:acu-input-root、acu-input-header、acu-input-editor、acu-input-footer、acu-input-trigger、acu-input-counter、acu-input-submit、acu-menu、acu-menu-header、acu-menu-list、acu-menu-group、acu-menu-group-heading、acu-menu-item、acu-menu-empty、acu-mention、acu-mention-chip。
主题尊重 prefers-reduced-motion: reduce。
⌨️ 键盘
| submitKey | 提交 | 换行 |
|-------------|------|------|
| 'Enter'(默认) | Enter | Shift+Enter |
| 'Ctrl+Enter' | Ctrl+Enter | Enter |
| 'Meta+Enter' | macOS Cmd+Enter,Windows / Linux Ctrl+Enter | Enter |
未传 onSubmit 时提交键不被接管,Enter 换行。面板打开时的按键见 MentionMenu 键盘。
📖 API 参考
@bdky/agent-chat-ui/core
| 导出 | 类型 | 说明 |
|------|------|------|
| InputSlot | TipTap Node | 行内插槽节点 |
| SubmitShortcut | TipTap Extension | 键盘提交 |
| AgentMention | TipTap Node | 标签节点 |
| CharacterLimit | TipTap Extension | 字数统计与拦截 |
| docToPlainText / extractMentions / extractSlotValues / buildTemplateHTML / sanitizeText | 函数 | 见 工具函数 |
| createMentionMenuStore | 函数 | 下拉面板 store |
| AgentInputOptions | 接口 | useAgentInput 参数 |
| MentionTrigger / MentionMenuItem / MentionValue | 接口 | 触发字符配置 / 候选项 / 标签值 |
| MentionMenuStore / MentionMenuSnapshot | 接口 | 面板 store / 状态快照 |
| InputSlotOptions / SubmitShortcutOptions / AgentMentionOptions / CharacterLimitOptions / CharacterLimitStorage | 接口 | 扩展选项 |
| SlotConfig / SlotDef / InputSlotAttrs / AgentInputState / SlotRenderer | 接口 / 类型 | 插槽与状态 |
@bdky/agent-chat-ui/react
| 导出 | 类型 | 说明 |
|------|------|------|
| useAgentInput | Hook | 主 hook |
| AgentInput | 组件 + 部件 | 预设组件;AgentInput.Root / Header / Editor / Footer / TriggerButton / Counter / Submit |
| MentionMenu | 部件 | MentionMenu.Root / Header / List / Group / Item / Empty |
| MentionChip | 组件 | 标签默认渲染 |
| MentionPanel / MentionNodeView | 组件 | 直接配置 AgentMention 扩展时使用 |
| InputSlotView | 组件 | 插槽节点视图 |
| AgentInputContext / useAgentInputContext | Context / Hook | 编辑器状态上下文 |
| UseAgentInputReturn / InsertMentionAttrs / MentionRenderProps | 接口 | hook 返回值 / insertMention 参数 / renderMention 参数 |
| AgentInputRootProps / AgentInputSectionProps / AgentInputEditorProps / AgentInputTriggerButtonProps / AgentInputCounterProps / AgentInputCounterContext / AgentInputSubmitProps | 接口 | AgentInput.* 属性 |
| MentionMenuRootProps / MentionMenuHeaderProps / MentionMenuListProps / MentionMenuGroupProps / MentionMenuItemProps / MentionMenuEmptyProps | 接口 | MentionMenu.* 属性 |
| AgentInputProps / AgentInputSlotContext / AgentInputToolbarContext / AgentInputContextValue | 接口 | 预设组件与上下文 |
| MentionPanelProps / MentionPanelRef / MentionItem / MentionNodeViewProps | 接口 | MentionPanel / MentionNodeView |
🔧 高级用法
发送失败时放回原内容
提交前保存 JSON 快照(含标签),失败时恢复:
const input = useAgentInput({
onSubmit: async content => {
const snapshot = input.editor?.getJSON();
input.clear();
try {
await send(content);
}
catch {
input.editor?.commands.setContent(snapshot ?? '');
}
}
});没有发送键的输入框
不传 onSubmit,Enter 即为换行;不渲染 AgentInput.Submit。
按状态切换占位文本
placeholder 变化即时生效:
const input = useAgentInput({placeholder: running ? '任务运行中,可继续补充要求' : '描述任务'});添加自定义 TipTap 扩展
import {Extension} from '@tiptap/core';
const BlurOnEscape = Extension.create({
name: 'blurOnEscape',
addKeyboardShortcuts() {
return {Escape: () => this.editor.commands.blur()};
}
});
const input = useAgentInput({extensions: [BlurOnEscape]});editor 是标准 TipTap Editor 实例,可直接使用其全部 API。
🌍 浏览器支持
| 浏览器 | 版本 | |--------|------| | Chrome | >= 74 | | Firefox | >= 90 | | Safari | >= 14.1 | | Edge | >= 79 | | iOS Safari | >= 14.1 | | Android Chrome | >= 74 |
默认皮肤的聚焦光圈使用 color-mix()(Chrome 111+、Safari 16.2+、Firefox 113+);更早的浏览器中边框颜色照常变化,只是没有光圈。
📄 许可证
MIT
Made with ❤️ by 百度智能云客悦 Ky-FE Team
