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

@bdky/agent-chat-ui

v0.2.0

Published

AI 对话界面 UI 组件库,基于 Tiptap 富文本编辑器,提供开箱即用的 Agent 聊天 React 组件

Readme

@bdky/agent-chat-ui

npm version License: MIT TypeScript

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