使用场景
在 AI 聊天场景下,用户可通过 AIChatInput实现富文本输入、上传、引用、建议、模版、功能配置、及丰富自定义展示等需求。
AIChatInput 的富文本输入是基于 tiptap 实现,tiptap 是一款现代的富文本编辑器开发框架,支持 React、Vue 前端框架,具备极强的可定制性和扩展性。其组件化能力优秀,性能优良,内置多种常用拓展,并支持用户自定义节点、命令、插件与菜单,使复杂 AI 场景下的富文本输入能力能够灵活适配和扩展。Semi 的 AIChatInput 组件对 tiptap 进行了封装,开发者可开箱即用或按需按业务扩展。
代码演示
如何引入
import { AIChatInput } from '@fibrous/semi-ai-react';
基本用法
支持文本输入以及文件上传,使用时可按需配置以下参数:
uploadProps配置文件上传相关的参数,详见 UploadPropsonUploadChange获取文件上传变化- 删除上传文件时,会触发
uploadProps.onRemove,并遵循uploadProps.beforeRemove(支持 Promise) placeholder输入框的占位符defaultContent输入框的默认内容onContentChange输入框内容变化时的回调函数,参数为当前输入框的内容
代码
import { useCallback } from 'react';
import { AIChatInput, type AIChatInputContent as Content } from '@fibrous/semi-ai-react';
import type { UploadOnChangeProps as OnChangeProps } from '@fibrous/semi-ui-react';
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
export default function AIChatInputDemo() {
const onContentChange = useCallback((content: Content[]) => {
console.log('onContentChange', content);
}, []);
const onUploadChange = useCallback((fileList: OnChangeProps) => {
console.log('onUploadChange', fileList);
}, []);
return <AIChatInput placeholder="输入内容或者上传内容..." uploadProps={uploadProps} onContentChange={onContentChange} onUploadChange={onUploadChange} style={outerStyle} />;
}消息发送
当输入框中有内容(包括输入文本、上传内容),将允许发送消息。点击消息发送按钮,会触发 onMessageSend 回调函数,参数为当前输入框的内容,包括输入区域的文本,引用内容,上传文件,配置区域内容。引用内容不参与是否可发送的判断,也可通过 canSend 直接指定。
用户可在 onMessageSend 中根据判断是否设置 generating 表示消息正在处理中,如果 generating 为 true,则 AIChatInput 会在发送按钮位置显示停止生成按钮,并清空输入区的消息,以及上传文件,另外,引用内容需要用户自行清除。
点击停止生成按钮,会触发 onStopGenerate 回调函数,用户可在该回调函数中处理停止生成的逻辑, 如将 generating 设为 false。
代码
import { useState, useCallback } from 'react';
import { AIChatInput, type AIChatInputContent as Content, type AIChatInputReference as Reference } from '@fibrous/semi-ai-react';
import type { UploadOnChangeProps as OnChangeProps, UploadProps } from '@fibrous/semi-ui-react';
const uploadProps: UploadProps = {
action: 'https://api.semi.design/upload',
defaultFileList: [
{
uid: '1',
name: 'dy.jpeg',
status: 'success',
size: '130kb',
url: '/assets/placeholder/avatar-a.svg',
},
{
uid: '5',
name: 'resso.jpeg',
status: 'uploading',
percent: 50,
size: '222kb',
url: '/assets/placeholder/avatar-b.svg',
},
],
};
const outerStyle = { margin: 12 };
const reference: Reference[] = [
{
id: '1',
type: 'text',
content: '测试文本,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字',
},
];
export default function AIChatInputDemo() {
const [references, setReferences] = useState(reference);
const [generating, setGenerating] = useState(false);
const onContentChange = useCallback((content: Content[]) => {
console.log('onContentChange', content);
}, []);
const onUploadChange = useCallback((fileList: OnChangeProps) => {
console.log('onUploadChange', fileList);
}, []);
const toggleGenerate = useCallback(() => {
setGenerating((value) => !value);
}, []);
const onMessageSend = useCallback(() => {
toggleGenerate();
setReferences([]);
}, [toggleGenerate]);
const handleReferenceDelete = useCallback((item: Reference) => {
setReferences((references) => {
const newReference = references.filter((ref) => ref.id !== item.id);
return newReference;
});
}, []);
return (
<AIChatInput
defaultContent="点击发送按钮,观察上传内容、引用内容、输入框内容变化"
generating={generating}
uploadProps={uploadProps}
onContentChange={onContentChange}
onUploadChange={onUploadChange}
style={outerStyle}
onMessageSend={onMessageSend}
onStopGenerate={toggleGenerate}
onReferenceDelete={handleReferenceDelete}
references={references}
/>
);
}富文本输入区
AIChatInput 使用 tiptap 作为富文本输入框的编辑器,用户可以在输入框中输入文本,使用 AIChatInput 内置的 extensions(包括 input-slot,select-slot,skill-slot)。用户也可以自定义 extensions 来扩展编辑器的功能。
input-slot支持用户输入文本,并支持 placeholder 占位符。select-slot支持用户进行简单的选择,选项仅支持 string 类型。skill-slot是用于技能展示的块,方便用户理解当前输入框中的技能。
可以通过 ref 方法 setContent 来设置输入框的内容,使用 focusEditor 方法可以将输入框的焦点设置到编辑器中。
代码
import { useRef, useCallback, useState, type KeyboardEvent, type MouseEvent } from 'react';
import { AIChatInput, type AIChatInputHandle } from '@fibrous/semi-ai-react';
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
const temp = {
'input-slot': '我是一个<input-slot placeholder="[职业]">程序员</input-slot>',
'select-slot': `我是<select-slot value="前端开发" options='["设计","前端开发","后端开发"]'></select-slot>,帮我完成...`,
'skill-slot': `<skill-slot data-label="AI Coding" data-value="AI Coding" data-template=false></skill-slot> 帮我完成...`,
};
export default function AIChatInputDemo() {
const [activeIndex, setActiveIndex] = useState(0);
const ref = useRef<AIChatInputHandle>(null);
const setTemplate = useCallback(
(event: MouseEvent<HTMLDivElement> | KeyboardEvent<HTMLDivElement>) => {
const index = Number((event.target as HTMLElement).dataset.index);
setActiveIndex(index);
const content = Object.values(temp)[index];
if (ref.current) {
ref.current.setContent(content);
ref.current.focusEditor();
}
},
[ref]
);
return (
<>
<div className="aiChatInput-radio" role="tablist">
{Object.keys(temp).map((item, index) => {
return (
<div
className={`aiChatInput-radio-item ${index === activeIndex ? 'aiChatInput-radio-item-selected' : ''}`}
key={index}
data-index={index}
role="tab"
aria-selected={index === activeIndex}
tabIndex={0}
onClick={setTemplate}
onKeyDown={(e) => {
if (e.key === 'Enter') {
setTemplate(e);
}
}}
>
{item}
</div>
);
})}
</div>
<AIChatInput ref={ref} defaultContent={temp['input-slot']} placeholder="输入内容或者上传内容" uploadProps={uploadProps} style={outerStyle} />
</>
);
}引用
用户可以 references 传入引用内容,引用内容会展示在输入框的顶部。
renderReference自定义单个引用内容的渲染。onReferenceDelete处理引用内容的删除。onReferenceClick处理引用内容的点击。
代码
import { useState, useCallback } from 'react';
import { AIChatInput, type AIChatInputReference as Reference } from '@fibrous/semi-ai-react';
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
const referenceTemp = [
{
id: '1',
type: 'text',
content: '测试文本,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字',
},
{
id: '2',
name: '飞书文档.docx',
},
{
id: '3',
name: '飞书文档.pdf',
},
{
id: '4',
name: 'Music.mp4',
},
{
id: '5',
name: 'Image.jpeg',
url: '/assets/placeholder/avatar-b.svg',
},
{
id: '6',
name: 'code.json',
},
];
export default function AIChatInputDemo() {
const [references, setReferences] = useState(referenceTemp);
const handleReferenceDelete = useCallback(
(item: Reference) => {
const newReference = references.filter((ref) => ref.id !== item.id);
setReferences(newReference);
},
[references]
);
const handleReferenceClick = useCallback((item: Reference) => {
console.log('点击了引用', item);
}, []);
return <AIChatInput placeholder="用于查看引用内容的用例" onReferenceDelete={handleReferenceDelete} onReferenceClick={handleReferenceClick} references={references} uploadProps={uploadProps} style={outerStyle} />;
}配置区域
用户可以通过配置区域设置使用模型参数、联网搜索、深度思考等配置项,展示或者查看 MCP 工具。
可通过 renderConfigureArea API 自定义输入框的操作按钮。
使用 Configure 中的 Select、Button、Mcp、RadioButton 等组件可以自定义配置项。
Configure 将管理配置项的状态,用户可以通过 onConfigureChange API 监听配置项的变化。一定要配置 field 属性,用于标识配置项的唯一标识。如需设置初始值,可通过 initValue 属性设置。
如果用户有其他形式的配置需求,可以通过 getConfigureItem 将自定义组件扩展成 Configure 类型组件。
代码
import { useCallback } from 'react';
import { AIChatInput, type AIChatInputLeftMenuChangeProps as LeftMenuChangeProps } from '@fibrous/semi-ai-react';
import { IconBookOpenStroked, IconFeishuLogo, IconGit, IconFigma } from '@fibrous/semi-icons-react';
const { Configure } = AIChatInput;
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
const modelOptions = [
{ value: 'GPT-5', label: 'GPT-5' },
{ value: 'GPT-4o', label: 'GPT-4o' },
{ value: 'Claude 3.5 Sonnet', label: 'Claude 3.5 Sonnet' },
];
const mcpOptions = [
{ icon: <IconFeishuLogo />, label: '飞书文档', value: 'feishu' },
{ icon: <IconGit />, label: 'Github Mcp', value: 'github' },
{ icon: <IconFigma />, label: 'IconFigma Mcp', value: 'IconFigma' },
];
const radioButtonProps = [
{ label: '极速', value: 'fast' },
{ label: '思考', value: 'think' },
{ label: '超能', value: 'super' },
];
export default function AIChatInputDemo() {
const onConfigureButtonClick = useCallback(() => {
console.log('onConfigureButtonClick');
}, []);
const renderLeftMenu = useCallback(
() => (
<>
<Configure.Select optionList={modelOptions} field="model" initValue="GPT-4o" />
<Configure.Button icon={<IconBookOpenStroked />} field="onlineSearch">
联网搜索
</Configure.Button>
<Configure.Mcp options={mcpOptions} onConfigureButtonClick={onConfigureButtonClick} showConfigure />
<Configure.RadioButton options={radioButtonProps} field="thinkType" initValue="fast" />
</>
),
[onConfigureButtonClick]
);
const onConfigureChange = useCallback((value: LeftMenuChangeProps, changedValue?: LeftMenuChangeProps) => {
console.log('onConfigureChange', value, changedValue);
}, []);
return <AIChatInput placeholder="用于查看左下方配置项的用例" renderConfigureArea={renderLeftMenu} onConfigureChange={onConfigureChange} uploadProps={uploadProps} style={outerStyle} />;
}使用 getConfigureItem 扩展自定义组件为 Configure 类型组件。
function getConfigureItem<P, VK extends keyof P = 'value', CK extends keyof P = 'onChange'>(
component: React.ComponentType<P>,
opts?: {
// 指定需要接管的组件的 value 对应的 key,默认是 value
valueKey?: VK;
// 指定需要接管的组件的值变化对应的函数名,默认是 onChange
onKeyChangeFnName?: CK;
// 指定需要接管的组件的值变化函数参数中 value 对应的路径,默认取参数本身
valuePath?: string;
className?: string;
defaultProps?: Partial<P>;
}
): React.ComponentType<
Omit<P, VK | CK | 'onChange' | 'className'> & {
// 配置项在 Configure 值中的 key,必填
field: string;
initValue?: P[VK];
onChange?: (value: unknown) => void;
className?: string;
}
>;
返回的组件按被包装组件的 Props 检查属性,field 必填,initValue 的类型与 valueKey 对应的属性一致。onChange 由配置项占用,收到该项的新值,被包装组件自己的 onChange 不在可接受的属性中。
使用示例如下:
代码
import { AIChatInput, getConfigureItem, type AIChatInputLeftMenuChangeProps as LeftMenuChangeProps } from '@fibrous/semi-ai-react';
import { Cascader, type CascaderProps } from '@fibrous/semi-ui-react';
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
const cascaderModalOptions = [
{
label: 'GPT',
value: 'GPT',
children: [
{ label: 'GPT-4o', value: 'GPT-4o' },
{ value: 'GPT-5', label: 'GPT-5' },
],
},
{
label: 'Claude',
value: 'Claude',
children: [{ label: 'Claude 3.5 Sonnet', value: 'Claude 3.5 Sonnet' }],
},
];
const myCascader = (props: CascaderProps) => {
return <Cascader {...props} />;
};
const CustomCascader = getConfigureItem(myCascader, {
className: 'aiChatInput-cascader-configure',
});
export default function AIChatInputDemo() {
const renderLeftMenu = () => <CustomCascader field="model" treeData={cascaderModalOptions} initValue={['GPT', 'GPT-4o']} />;
const onConfigureChange = (value: LeftMenuChangeProps, changedValue?: LeftMenuChangeProps) => {
console.log('onConfigureChange', value, changedValue);
};
return <AIChatInput placeholder="用于查看左下方配置项的用例" renderConfigureArea={renderLeftMenu} onConfigureChange={onConfigureChange} uploadProps={uploadProps} style={outerStyle} />;
}操作区域
输入框右下角为操作区域,用户可以通过 renderActionArea API 自定义操作区域,展示自定义的操作按钮。menuItem 依次是上传节点(showUploadButton 为 false 时是隐藏的 Upload,仍需渲染才能粘贴上传)与发送或停止按钮。
interface ActionAreaProps {
menuItem: ReactNode[];
className: string;
}
使用示例如下:
代码
import { useCallback } from 'react';
import { AIChatInput, type AIChatInputActionAreaProps as ActionAreaProps } from '@fibrous/semi-ai-react';
import { IconDeleteStroked } from '@fibrous/semi-icons-react';
import { Divider, Button } from '@fibrous/semi-ui-react';
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
export default function AIChatInputDemo() {
const renderActionArea = useCallback((props: ActionAreaProps) => {
return (
<div className={props.className}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Button aria-label="清空内容" type="tertiary" style={{ borderRadius: '50%' }} icon={<IconDeleteStroked />} />
<Divider layout="vertical" style={{ marginLeft: 8 }} />
</div>
{props.menuItem}
</div>
);
}, []);
return <AIChatInput renderActionArea={renderActionArea} placeholder="输入内容或者上传内容..." uploadProps={uploadProps} style={outerStyle} />;
}自定义上传按钮
底部操作区左侧默认会渲染上传按钮。你可以通过 renderUploadButton 仅自定义按钮 UI(例如改成图标按钮、加 Tooltip 等)。
注意:这不会影响上传/粘贴上传逻辑(Upload 仍由组件内部托管),openFileDialog 会触发内部 Upload 的文件选择。
代码
import { AIChatInput } from '@fibrous/semi-ai-react';
import { IconUpload } from '@fibrous/semi-icons-react';
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
export default function AIChatInputDemo() {
return (
<AIChatInput
placeholder="自定义上传按钮(仍支持粘贴上传)"
uploadProps={uploadProps}
renderUploadButton={({ openFileDialog, disabled }) => (
<button
type="button"
aria-label="上传"
disabled={disabled}
className="semi-button semi-button-borderless"
onClick={(e) => {
e.stopPropagation();
openFileDialog();
}}
>
<IconUpload />
</button>
)}
style={outerStyle}
/>
);
}底部按钮形状
用户可以通过 round API 配置底部按钮的形状,默认是 true,是圆角按钮, 可以设置为 false 来配置为方形按钮。
代码
import { useState, useCallback } from 'react';
import { AIChatInput } from '@fibrous/semi-ai-react';
import { IconBookOpenStroked, IconFeishuLogo, IconGit, IconFigma } from '@fibrous/semi-icons-react';
import { RadioGroup, Radio, type RadioChangeEvent } from '@fibrous/semi-ui-react';
const { Configure } = AIChatInput;
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
const modelOptions = [
{ value: 'GPT-5', label: 'GPT-5' },
{ value: 'GPT-4o', label: 'GPT-4o' },
{ value: 'Claude 3.5 Sonnet', label: 'Claude 3.5 Sonnet' },
];
const mcpOptions = [
{ icon: <IconFeishuLogo />, label: '飞书文档', value: 'feishu' },
{ icon: <IconGit />, label: 'Github Mcp', value: 'github' },
{ icon: <IconFigma />, label: 'IconFigma Mcp', value: 'IconFigma' },
];
const radioButtonProps = [
{ label: '极速', value: 'fast' },
{ label: '思考', value: 'think' },
{ label: '超能', value: 'super' },
];
export default function AIChatInputDemo() {
const [round, setRound] = useState(false);
const renderLeftMenu = useCallback(
() => (
<>
<Configure.Select optionList={modelOptions} field="model" initValue="GPT-4o" />
<Configure.Button icon={<IconBookOpenStroked />} field="onlineSearch">
联网搜索
</Configure.Button>
<Configure.Mcp options={mcpOptions} />
<Configure.RadioButton options={radioButtonProps} field="mode" initValue="fast" />
</>
),
[]
);
const onChange = useCallback((e: RadioChangeEvent) => {
setRound(e.target.value);
}, []);
return (
<>
<RadioGroup onChange={onChange} value={round} aria-label="单选组合示例" name="demo-radio-group">
<Radio value>圆形</Radio>
<Radio value={false}>方形</Radio>
</RadioGroup>
<AIChatInput placeholder="下方按钮为方形的用例" round={round} renderConfigureArea={renderLeftMenu} uploadProps={uploadProps} style={outerStyle} />
</>
);
}建议
用户可通过 suggestions API 配置建议列表,功能类似于 AutoComplete 组件,用户可以根据输入的内容实现根据输入的内容动态展示建议列表。
使用键盘上下方向键切换建议列表的选项。按下 ESC 或者点击非建议列表,输入框区域,建议列表将关闭。
还可通过 renderSuggestionItem API 自定义建议列表的展示,需要把参数里的 id、role、aria-selected 与事件处理展开到根元素上。
代码
import { useState, useCallback } from 'react';
import { AIChatInput, type AIChatInputContent as Content } from '@fibrous/semi-ai-react';
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
const suggestionTemplate = ['天气如何', '空气质量', '工作进程', '日程安排'];
export default function AIChatInputDemo() {
const [suggestion, setSuggestion] = useState<string[]>([]);
const onChange = useCallback((content: Content[]) => {
const value = content.length && content[0].text ? content[0].text : undefined;
if (value === undefined || value.includes('\n') || value.length >= 4) {
setSuggestion((current) => (current.length ? [] : current));
return;
}
setSuggestion(value.length ? suggestionTemplate.map((item) => `${value},${item}`) : []);
}, []);
return <AIChatInput suggestions={suggestion} onContentChange={onChange} uploadProps={uploadProps} style={outerStyle} placeholder="输入内容,当内容长度小于 4个字符可以看到建议,使用上下按键可切换侯选项" />;
}技能及模版
用户可以通过 skills API 配置技能列表,使用 skillHotKey 配置技能的触发键。
skills 的格式如下
interface Skill {
label?: string;
value?: string;
icon?: React.ReactNode;
// 技能是否有模版,有模版的技能被选中后,将在输入框的底部展示模版按钮
hasTemplate?: boolean;
}
由于模版的展示形式丰富,因此我们不提供默认的展示形式,用户可以通过 renderTemplateAPI 自定义模版的展示。模版面板的展示和关闭可通过点击模版按钮实现。
renderTemplate?: (
// 未选中技能、仅显示模版按钮时为 undefined
skill: Skill | undefined,
// 模版点击回调,点击模版后,用模版的内容替换输入框的内容
onTemplateClick: (content: string) => void
) => ReactNode;
使用示例如下:
代码
import { useState, useCallback, useRef, type MouseEvent } from 'react';
import { AIChatInput, type AIChatInputHandle, type AIChatInputSkill as Skill } from '@fibrous/semi-ai-react';
import { IconTemplateStroked, IconSearch } from '@fibrous/semi-icons-react';
const { Configure } = AIChatInput;
const modelOptions = [
{ value: 'GPT-5', label: 'GPT-5' },
{ value: 'GPT-4o', label: 'GPT-4o' },
{ value: 'Claude 3.5 Sonnet', label: 'Claude 3.5 Sonnet' },
];
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
const skills = [
{
icon: <IconTemplateStroked />,
value: 'writing',
label: '帮我写作',
hasTemplate: true,
},
{
icon: <IconSearch />,
value: 'AI 编程',
label: 'AI coding',
},
];
const template = [
{
group: '工作',
children: [
{
bg: 'var(--semi-color-primary)',
icon: <IconTemplateStroked />,
title: '总结汇报',
desc: '凝练你的工作成效',
content: `我的职业是<input-slot placeholder="[请输入职业]"></input-slot>,帮我写一份关于<input-slot placeholder="[输入目的:项目进展总结、团队工作成果或其他]"></input-slot>的总结汇报`,
},
{
bg: 'var(--semi-color-warning)',
icon: <IconTemplateStroked />,
title: '话术',
desc: '满足不同场景表达需求',
content: `我是一名<select-slot value="打工人" options='["打工人","学生"]'></select-slot> ,帮我写一段面向<input-slot placeholder="[输入对象]">陌生同事</input-slot>的话术内容`,
},
],
},
{
group: '商业营销',
children: [
{
bg: 'var(--semi-color-primary)',
icon: <IconTemplateStroked />,
title: '宣传文案',
desc: '撰写各平台的推广文案',
content: '帮我写一篇面向<input-slot placeholder="[输入目标人群]"></input-slot>职场人士,关于<input-slot placeholder="[输入产品]"></input-slot>的宣传文案,需要直击痛点,吸引用户点击。',
},
{
bg: 'var(--semi-color-warning)',
icon: <IconTemplateStroked />,
title: '方案策划',
desc: '量身定制各种方案',
content: '我是一名<input-slot placeholder="[输入职业]"></input-slot>职业策划人 ,帮我写一个<input-slot placeholder="[方案类型:如线下读书会活动方案等]"></input-slot>线下读书会活动 的方案,需要包含但不限于策划目标、详细计划、所需资源和预算、效果评估、风险应对等。',
},
],
},
];
interface TemplateContentProps {
onTemplateClick: (content: string) => void;
}
const TemplateContent = ({ onTemplateClick: onTemplateClickProps }: TemplateContentProps) => {
const [groupIndex, setGroupIndex] = useState(0);
const onItemClick = useCallback((e: MouseEvent<HTMLButtonElement>) => {
const index = e.currentTarget.dataset.index;
setGroupIndex(Number(index));
}, []);
const onTemplateClick = useCallback(
({ content }: { content: string }) => {
onTemplateClickProps(content);
},
[onTemplateClickProps]
);
return (
<div className="aiChatInput-template">
<div className="template-header">
{template.map((item, index) => {
return (
<button type="button" key={index} data-index={index} className={`template-header-item ${groupIndex === index ? 'template-header-item-active' : ''}`} style={{ border: 'none', fontFamily: 'inherit' }} onClick={onItemClick}>
{item.group}
</button>
);
})}
</div>
<div className="template-content">
{(template[groupIndex]?.children ?? []).map((item, index) => (
<button type="button" key={index} className="template-content-item" style={{ font: 'inherit', textAlign: 'left' }} onClick={() => onTemplateClick(item)}>
<span className="template-content-item-icon" style={{ background: item.bg }}>
{item.icon}
</span>
<span className="template-content-item-title">{item.title}</span>
<span className="template-content-item-desc">{item.desc}</span>
</button>
))}
</div>
</div>
);
};
export default function AIChatInputDemo() {
const ref = useRef<AIChatInputHandle>(null);
const setTemplate = useCallback(
(content: string) => {
const element = ref.current;
if (!element) {
return;
}
element.setContentWhileSaveTool(content);
element.focusEditor();
},
[ref]
);
const renderTemplate = useCallback(
(skill?: Skill) => {
if (skill?.value === 'writing') {
return <TemplateContent onTemplateClick={setTemplate} />;
}
},
[setTemplate]
);
const renderLeftMenu = useCallback(
() => (
<>
<Configure.Select optionList={modelOptions} field="model" initValue="GPT-4o" />
</>
),
[]
);
return <AIChatInput placeholder="输入 / 唤起技能,选择技能后,点击模版按钮可查看模版,可通过鼠标上下按键切换侯选项" renderConfigureArea={renderLeftMenu} ref={ref} uploadProps={uploadProps} skills={skills} skillHotKey="/" renderTemplate={renderTemplate} style={outerStyle} />;
}自定义渲染顶部区域
用户可以通过 renderTopSlot API 自定义渲染顶部区域,可自行渲染引用,上传内容以及配置项。可结合 showReference 和 showUploadFile API 控制是否展示引用和上传文件。另外,可通过 topSlotPosition API 配置自定义渲染内容相对于引用区域,上传展示区域的相对位置。
interface TopSlotProps {
// 引用
references: Reference[];
// 上传内容
attachments: Attachment[];
// 输入框消息
content: Content[];
handleUploadFileDelete: (attachment: Attachment) => void;
handleReferenceDelete: (reference: Reference) => void;
}
使用用例如下:
代码
import React, { useState, useCallback, useRef } from 'react';
import { AIChatInput, type AIChatInputHandle, type AIChatInputAttachment as Attachment, type AIChatInputReference as Reference, type AIChatInputRenderTopSlotProps as RenderTopSlotProps } from '@fibrous/semi-ai-react';
import { IconClose, IconUpload, IconFile, IconFolder, IconBranch, IconTerminal, IconGlobeStroke, IconConnectionPoint2, IconTemplateStroked, IconSearch, IconGit, IconCode } from '@fibrous/semi-icons-react';
const { Configure } = AIChatInput;
const radioButtonProps = [
// the icon is the only label, so it carries the name
{ label: <IconTemplateStroked aria-label="快速" />, value: 'fast' },
{ label: <IconSearch aria-label="深度思考" />, value: 'think' },
];
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
const customReferences: Reference[] = [
{
type: 'file',
id: '1',
name: 'horizontalScroller.tsx',
path: 'packages/semi-ui-react/AIChatInput/horizontalScroller.tsx',
},
{
type: 'folder',
id: '2',
name: 'AIChatInput',
path: 'packages/semi-ui-react/AIChatInput',
},
{
type: 'web',
id: '3',
name: 'web',
},
{
type: 'change',
id: '4',
name: 'recentChange',
},
{
type: 'branch',
id: '5',
name: 'Branch',
detail: 'Diff with Main Branch',
branch: 'feat/aichatinput',
targetBranch: 'feat/targetBranch',
},
{
type: 'terminal',
id: '6',
name: 'From 1-2',
from: 1,
to: 2,
},
];
function isImageType(item: Attachment) {
const PIC_PREFIX = 'image/';
const PIC_SUFFIX_ARRAY = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'];
const { name = '', fileInstance } = item;
const suffix = name.split('.').pop() ?? '';
let result = false;
const type = fileInstance?.type ?? '';
if (type.startsWith(PIC_PREFIX)) {
result = true;
} else if (PIC_SUFFIX_ARRAY.includes(suffix)) {
result = true;
}
return result;
}
const refTypeToIconMap = new Map([
['file', <IconFile key="file" size="small" />],
['folder', <IconFolder key="folder" size="small" />],
['branch', <IconBranch key="branch" size="small" />],
['terminal', <IconTerminal key="terminal" size="small" />],
['web', <IconGlobeStroke key="globalStroke" size="small" />],
['change', <IconConnectionPoint2 key="connectionPoint2" size="small" />],
['git', <IconGit key="git" size="small" />],
['code', <IconCode key="code" size="small" />],
]);
export default function AIChatInputDemo() {
const ref = useRef<AIChatInputHandle>(null);
const [reference, setReference] = useState(customReferences);
const renderLeftMenu = useCallback(
() => (
<>
<Configure.RadioButton options={radioButtonProps} initValue="fast" field="mode" />
</>
),
[]
);
const renderTopSlot = useCallback(({ attachments = [], references = [] }: RenderTopSlotProps) => {
return (
<div className="ai-chat-input-topSlot">
{references.map(({ type, name, detail, id }, index) => {
const icon = type ? refTypeToIconMap.get(type) : undefined;
return (
<div className="item" key={id}>
<span className="item-icon">
{icon && React.cloneElement(icon, { className: 'item-left item-icon' })}
<button
type="button"
className="item-icon-delete"
aria-label="删除"
onClick={() => {
const newReferences = [...references];
newReferences.splice(index, 1);
setReference(newReferences);
}}
>
<IconClose size="small" />
</button>
</span>
<span className="item-content">
{name}
{type === 'branch' && <span className="detail">{detail}</span>}
</span>
</div>
);
})}
{attachments.map((item) => {
const isImage = isImageType(item);
const { uid, name } = item;
return (
<div className="item" key={uid}>
<span className="item-icon">
{isImage ? <img className="item-image item-left" src={item.url} alt={item.name} /> : <IconUpload size="small" className="item-left item-icon" />}
<button
type="button"
className="item-icon-delete"
aria-label="删除"
onClick={() => {
ref.current?.deleteUploadFile(item);
}}
>
<IconClose size="small" />
</button>
</span>
<span className="item-content">{name}</span>
</div>
);
})}
</div>
);
}, []);
return <AIChatInput className="aiChatInput-customTopSlot" renderTopSlot={renderTopSlot} references={reference} showUploadFile={false} showReference={false} renderConfigureArea={renderLeftMenu} ref={ref} uploadProps={uploadProps} style={outerStyle} placeholder="自定义渲染顶部内容,可用于渲染上传内容、引用内容" />;
}自定义扩展
富文本区域可以自定义扩展,自定义扩展的实现可参考 Tiptap 自定义扩展。通过 extensions API 可将自定义扩展添加到 AIChatInput 组件中。
如果添加了自定义扩展,需要在 transformer 中添加对应的转换规则, 以保证在 onContentChange 中得到的该节点数据符合用户预期。
添加自定义扩展时有以下注意事项:
- 请在自定义扩展中添加
isCustomSlot的属性,该属性和自定义扩展前后的光标高度有关 - 由于
AIChatInput使用Enter作为发送热键,如果自定义扩展有使用Enter作为快捷操作,需要自行设置editor.storage中的SemiAIChatInput.allowHotKeySend用于表示热键是否应该被 AIChatInput 用于发送,避免热键冲突。sendHotKey为shift+enter时,allowHotKeySend为false期间单独按下的Enter不再换行,交给自定义扩展处理
自定义扩展定义及注意事项的示例如下:
代码
import React, { useState, useCallback, useRef, useMemo, useEffect, useImperativeHandle, type ReactNode, type Ref } from 'react';
import { computePosition, shift } from '@floating-ui/dom';
import { Node, mergeAttributes, type Editor } from '@tiptap/core';
import { Mention, type MentionNodeAttrs, type MentionOptions } from '@tiptap/extension-mention';
import { ReactNodeViewRenderer, NodeViewWrapper, posToDOMRect, ReactRenderer, type ReactNodeViewProps } from '@tiptap/react';
import { AIChatInput, type AIChatInputHandle, type AIChatInputAttachment as Attachment, type AIChatInputContent as Content, type AIChatInputReference as Reference, type AIChatInputRenderTopSlotProps as RenderTopSlotProps } from '@fibrous/semi-ai-react';
import { IconFile, IconFolder, IconBranch, IconCode, IconGit, IconGlobeStroke, IconChevronRight, IconClose, IconUpload, IconTerminal, IconConnectionPoint2 } from '@fibrous/semi-icons-react';
const uploadProps = { action: 'https://api.semi.design/upload' };
const outerStyle = { margin: 12 };
interface PanelItem {
icon: ReactNode;
key: string;
type: string;
name: string;
path?: string;
}
const TestAction: Record<string, PanelItem[]> = {
'Files & Folders': [
{
icon: <IconFile />,
key: '1-1',
type: 'file',
name: 'TagInput.scss',
path: 'package/semi-founctaion/TagInput.scss',
},
{
icon: <IconFolder />,
key: '1-2',
type: 'folder',
name: 'package',
path: '/package',
},
{
icon: <IconFolder />,
key: '1-3',
type: 'folder',
name: 'semi-ui',
path: '/package/semi-ui',
},
],
Git: [
{
icon: <IconBranch />,
key: '2-1',
type: 'branch',
name: 'fix/tag',
},
{
icon: <IconCode />,
key: '2-2',
type: 'code',
name: 'v2.86.0',
path: '/package',
},
{
icon: <IconGit />,
key: '2-3',
type: 'git',
name: 'chore: publish',
},
],
};
const FirstLevel = Object.keys(TestAction);
function ReferSlotComponent({ node }: ReactNodeViewProps) {
const value = node.attrs.value ? node.attrs.value : '';
return (
<NodeViewWrapper className="ai-chat-input-refer-slot-wrapper">
<span className="ai-chat-input-refer-slot">{value}</span>
</NodeViewWrapper>
);
}
const ReferSlot = Node.create({
name: 'referSlot',
inline: true,
group: 'inline',
atom: true,
selectable: false,
addAttributes() {
return {
value: {
default: '输入内容',
parseHTML: (element) => element.getAttribute('data-value'),
renderHTML: (attributes) => ({
'data-value': attributes.value,
}),
},
info: {
default: '',
parseHTML: (element) => element.getAttribute('data-info'),
renderHTML: (attributes) => ({
'data-info': attributes.info,
}),
},
type: {
default: 'text',
parseHTML: (element) => element.getAttribute('data-type'),
renderHTML: (attributes) => ({
'data-type': attributes.type,
}),
},
uniqueKey: {
default: '',
parseHTML: (element) => element.getAttribute('data-unique-key'),
renderHTML: (attributes) => ({
'data-unique-key': attributes.uniqueKey,
}),
},
// Sizes the caret around the custom node; every custom node needs it
isCustomSlot: AIChatInput.getCustomSlotAttribute(),
};
},
parseHTML() {
return [
{
tag: 'refer-slot',
},
];
},
renderHTML({ HTMLAttributes }) {
return ['refer-slot', mergeAttributes(HTMLAttributes)];
},
addNodeView() {
return ReactNodeViewRenderer(ReferSlotComponent);
},
});
interface ReferSlotAttrs {
value: string;
info: string;
type?: string;
uniqueKey: string;
}
const updatePosition = (editor: Editor, element: HTMLElement) => {
const virtualElement = {
getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
};
void computePosition(virtualElement, element, {
placement: 'bottom-start',
strategy: 'absolute',
middleware: [shift()],
}).then(({ x, y, strategy }) => {
element.style.width = 'max-content';
element.style.position = strategy;
element.style.left = `${x}px`;
element.style.top = `${y}px`;
});
};
type MentionSuggestion = MentionOptions['suggestion'];
type MentionStartProps = Parameters<NonNullable<ReturnType<NonNullable<MentionSuggestion['render']>>['onStart']>>[0];
// Content the panel returns through command, not limited to Mention's own attributes
interface MentionCommandProps extends Partial<MentionNodeAttrs> {
item?: PanelItem;
allowHotKeySend?: boolean;
}
interface MentionListHandle {
onKeyDown: (props: { event: KeyboardEvent; exitCb?: () => void }) => boolean;
}
interface MentionListProps extends Omit<MentionStartProps, 'command'> {
ref?: Ref<MentionListHandle>;
command: (props: MentionCommandProps) => void;
}
const suggestion: MentionSuggestion = {
items: () => FirstLevel,
command: ({ editor, range, props }) => {
const { item, allowHotKeySend } = props as MentionCommandProps;
if (typeof allowHotKeySend === 'boolean') {
// AIChatInput registers its state under the extension name, which tiptap's Storage type does not know
(editor.storage as Record<string, { allowHotKeySend: boolean }>).SemiAIChatInput.allowHotKeySend = allowHotKeySend;
}
if (item) {
editor
.chain()
.focus()
.insertContentAt(range, {
type: 'referSlot',
attrs: {
type: item.type,
value: item.name || '',
info: JSON.stringify({ path: item.path }),
uniqueKey: item.key,
},
})
.run();
}
},
render: () => {
let component: ReactRenderer<MentionListHandle, MentionListProps>;
return {
onStart: (props) => {
component = new ReactRenderer(MentionList, {
props,
editor: props.editor,
});
if (!props.clientRect) return;
component.element.style.position = 'absolute';
document.body.appendChild(component.element);
updatePosition(props.editor, component.element);
},
onUpdate(props) {
component.updateProps(props);
if (!props.clientRect) return;
updatePosition(props.editor, component.element);
},
onKeyDown(props) {
function onExit() {
component.destroy();
}
return component.ref?.onKeyDown({ ...props, exitCb: onExit }) ?? false;
},
onExit() {
component.element.remove();
component.destroy();
},
};
},
};
const customReferences: Reference[] = [
{
type: 'file',
id: '1',
name: 'horizontalScroller.tsx',
path: 'packages/semi-ui-react/AIChatInput/horizontalScroller.tsx',
},
{
type: 'folder',
id: '2',
name: 'AIChatInput',
path: 'packages/semi-ui-react/AIChatInput',
},
{
type: 'web',
id: '3',
name: 'web',
},
{
type: 'change',
id: '4',
name: 'recentChange',
},
{
type: 'branch',
id: '5',
name: 'Branch',
detail: 'Diff with Main Branch',
branch: 'feat/aichatinput',
targetBranch: 'feat/targetBranch',
},
{
type: 'terminal',
id: '6',
name: 'From 1-2',
from: 1,
to: 2,
},
];
function MentionList({ ref, command, query, editor }: MentionListProps) {
const [level, setLevel] = useState(1);
const [options, setOptions] = useState<(string | PanelItem)[]>(FirstLevel);
const [selectedIndex, setSelectedIndex] = useState(0);
const [filterInput, setFilterInput] = useState({ options, query });
const filterOptions = query
? options.filter((item) => {
const name = typeof item === 'string' ? item : item.name;
return name.toLowerCase().includes(query.toLowerCase());
})
: options;
if (filterInput.options !== options || filterInput.query !== query) {
setFilterInput({ options, query });
setSelectedIndex(0);
}
useEffect(() => {
// While the panel is open, Enter belongs to it, not to AIChatInput's send
command({ allowHotKeySend: false });
return () => command({ allowHotKeySend: true });
}, [command]);
const selectItem = (item: string | PanelItem) => {
if (typeof item === 'string') {
setLevel(2);
setOptions(TestAction[item]);
} else {
command({ item });
}
};
const onItemClick = (item: string | PanelItem) => {
selectItem(item);
// Clicking an option blurs the editor, so return focus after entering the second level
if (level === 1) {
editor.commands.focus();
}
};
useImperativeHandle(ref, () => ({
onKeyDown: ({ event, exitCb }) => {
if (event.key === 'ArrowUp') {
setSelectedIndex((selectedIndex + filterOptions.length - 1) % filterOptions.length);
return true;
}
if (event.key === 'ArrowDown') {
setSelectedIndex((selectedIndex + 1) % filterOptions.length);
return true;
}
if (event.key === 'Enter') {
const item = filterOptions[selectedIndex];
if (item) {
selectItem(item);
}
return true;
}
if (event.key === 'Escape') {
if (level === 1) {
exitCb?.();
} else {
setLevel(1);
setOptions(FirstLevel);
}
return true;
}
return false;
},
}));
return (
<div role="menu" className="ai-chat-input-custom-extension-dropdown-menu" style={{ width: level === 1 ? 200 : 300 }}>
{filterOptions.length ? (
filterOptions.map((item, index) => (
<div
key={index}
role="menuitem"
tabIndex={-1}
className={index === selectedIndex ? 'is-selected optionItem' : 'optionItem '}
onClick={() => onItemClick(item)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onItemClick(item);
}
}}
onMouseEnter={() => setSelectedIndex(index)}
>
{typeof item === 'string' ? (
<span>{item}</span>
) : (
<div className="level2Item">
{item.icon}
<span className="name">{item.name}</span>
<span className="path">{item.path}</span>
</div>
)}
{level === 1 && <IconChevronRight className="option-item-arrow" />}
</div>
))
) : (
<div className="item">No result</div>
)}
</div>
);
}
function isImageType(item: Attachment) {
const PIC_PREFIX = 'image/';
const PIC_SUFFIX_ARRAY = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp'];
const { name = '', fileInstance } = item;
const suffix = name.split('.').pop() ?? '';
let result = false;
const type = fileInstance?.type ?? '';
if (type.startsWith(PIC_PREFIX)) {
result = true;
} else if (PIC_SUFFIX_ARRAY.includes(suffix)) {
result = true;
}
return result;
}
const refTypeToIconMap = new Map([
['file', <IconFile key="file" size="small" />],
['folder', <IconFolder key="folder" size="small" />],
['branch', <IconBranch key="branch" size="small" />],
['terminal', <IconTerminal key="terminal" size="small" />],
['web', <IconGlobeStroke key="globalStroke" size="small" />],
['change', <IconConnectionPoint2 key="connectionPoint2" size="small" />],
['git', <IconGit key="git" size="small" />],
['code', <IconCode key="code" size="small" />],
]);
export default function AIChatInputDemo() {
const ref = useRef<AIChatInputHandle>(null);
const [reference, setReference] = useState(customReferences);
const extensions = useMemo(() => {
return [
ReferSlot,
Mention.configure({
HTMLAttributes: {
class: 'mention',
},
suggestion,
}),
];
}, []);
const renderTopSlot = useCallback(({ attachments = [], references = [], content = [] }: RenderTopSlotProps) => {
const showContent = content.filter((item) => item.type !== 'text');
return (
<div className="ai-chat-input-topSlot">
{showContent.map((item, index) => {
const { type, value, name, key, detail } = item;
const icon = refTypeToIconMap.get(type);
return (
<div className="item" key={key ? key : index}>
<span className="item-icon">
{icon && React.cloneElement(icon, { className: 'item-left item-icon' })}
<button
type="button"
className="item-icon-delete"
aria-label="删除"
onClick={() => {
if (ref.current && ref.current.deleteContent) {
ref.current.deleteContent(item);
}
}}
>
<IconClose size="small" />
</button>
</span>
<span className="item-content">
{name ? name : value}
{type === 'branch' && <span className="detail">{detail}</span>}
</span>
</div>
);
})}
{references.map(({ type, name, detail, id }, index) => {
const icon = type ? refTypeToIconMap.get(type) : undefined;
return (
<div className="item" key={id}>
<span className="item-icon">
{icon && React.cloneElement(icon, { className: 'item-left item-icon' })}
<button
type="button"
className="item-icon-delete"
aria-label="删除"
onClick={() => {
const newReferences = [...references];
newReferences.splice(index, 1);
setReference(newReferences);
}}
>
<IconClose size="small" />
</button>
</span>
<span className="item-content">
{name}
{type === 'branch' && <span className="detail">{detail}</span>}
</span>
</div>
);
})}
{attachments.map((item) => {
const isImage = isImageType(item);
const { uid, name } = item;
return (
<div className="item" key={uid}>
<span className="item-icon">
{isImage ? <img className="item-image item-left" src={item.url} alt={item.name} /> : <IconUpload size="small" className="item-left item-icon" />}
<button
type="button"
className="item-icon-delete"
aria-label="删除"
onClick={() => {
if (ref.current && ref.current.deleteUploadFile) {
ref.current.deleteUploadFile(item);
}
}}
>
<IconClose size="small" />
</button>
</span>
<span className="item-content">{name}</span>
</div>
);
})}
</div>
);
}, []);
const onContentChange = useCallback((content: Content[]) => {
console.log('onContentChange', content);
}, []);
const transformer = useMemo(() => {
return new Map([
[
'referSlot',
({ attrs }: { attrs: ReferSlotAttrs }) => {
const { value, info, type = 'text', uniqueKey } = attrs;
return {
type: type,
value: value,
uniqueKey: uniqueKey,
...JSON.parse(info),
};
},
],
]);
}, []);
return (
<>
<AIChatInput
className="aiChatInput-customTopSlot"
renderTopSlot={renderTopSlot}
extensions={extensions}
references={reference}
showUploadFile={false}
showReference={false}
onContentChange={onContentChange}
ref={ref}
transformer={transformer}
uploadProps={uploadProps}
style={outerStyle}
placeholder="使用 @ 触发"
/>
</>
);
}API 参考
根元素带 aria-label 或 aria-labelledby 且未传 role 时,自动加 role="group"。附件卡的缩略图是装饰图(alt 为空),文件名由卡片内的可见文字提供。
AIChatInput
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| canSend | 是否可以发送。未设置时,根据输入框内容和上传内容决定;引用内容不参与判断。 | boolean | - |
| className | 根元素的类名。 | string | - |
| clearContentOnGenerating | generating 从 false 变为 true 时,是否清空输入框内容和附件。设为 false 可保留输入内容(适用于无限画布等非对话场景)。 | boolean | true |
| defaultContent | 输入框默认内容,支持 html string 以及 json 格式,同 Tiptap 的 Content;只在创建编辑器时读取。 | TiptapContent | - |
| dropdownMatchTriggerWidth | 下拉弹出层是否与输入框宽度一致。 | boolean | true |
| extensions | 自定义 tiptap 扩展;只在创建编辑器时读取,之后变化不生效。 | Extensions | - |
| generating | 是否正在生成中。为 true 时发送按钮变为停止生成按钮。 | boolean | false |
| immediatelyRender | 传给 tiptap 编辑器的 immediatelyRender 参数。为 false 时编辑器在挂载后创建,服务端渲染可用;为 true 时在首次渲染中创建,只用于纯客户端渲染。 | boolean | false |
| keepSkillAfterSend | 发送消息后是否保留已选的技能。 | boolean | false |
| onBlur | 富文本输入区失去焦点时调用,参数为焦点事件。 | Type(event: React.FocusEvent) => void | - |
| onConfigureChange | 配置区域的值变化时调用。移除配置项时 changedValue 为 undefined。 | Type(value: LeftMenuChangeProps, changedValue?: LeftMenuChangeProps) => void | - |
| onContentChange | 输入框内容变化时调用,参数为当前内容。 | Type(contents: Content[]) => void | - |
| onFocus | 富文本输入区获得焦点时调用,参数为焦点事件。 | Type(event: React.FocusEvent) => void | - |
| onMessageSend | 发送消息时调用(点击发送按钮或按发送快捷键),参数包含引用、上传文件、输入内容与配置区域的值。 | Type(props: MessageContent) => void | - |
| onPaste | 输入区粘贴时调用,不阻止默认的粘贴行为,可通过 event.clipboardData 读取内容。 | Type(event: ReactClipboardEvent<HTMLDivElement>) => void | - |
| onReferenceClick | 点击引用时调用,参数为该引用。 | Type(reference: Reference) => void | - |
| onReferenceDelete | 删除引用时调用,参数为被删除的引用;引用列表由使用方持有,需要在回调中移除。 | Type(reference: Reference) => void | - |
| onSkillChange | 技能切换时调用,移除技能时参数为 undefined。 | Type(skill?: Skill) => void | - |
| onStopGenerate | 点击停止生成按钮时调用。 | Type() => void | - |
| onUploadChange | 上传文件列表变化时调用。 | Type(props: OnChangeProps) => void | - |
| placeholder | 输入框占位符,可以是字符串,也可以是按节点返回字符串的函数。 | PlaceholderProps | - |
| popoverProps | 下拉弹出层的配置参数;className 与内部类名合并,style 与面板宽度合并。 | PopoverProps | - |
| ref | 命令式方法的引用,类型为 AIChatInputHandle,不指向根元素。 | TypeReact.Ref<AIChatInputHandle> | - |
| references | 输入框的引用列表。 | Reference[] | - |
| renderActionArea | 自定义底部的操作区域。参数 menuItem 依次为上传节点(showUploadButton 为 false 时是隐藏的 Upload,仍需渲染才能粘贴上传)与发送或停止按钮,className 为操作区域的类名。 | Type(props: ActionAreaProps) => ReactNode | - |
| renderConfigureArea | 自定义底部的配置区域,返回配置项(Configure 的子项)。 | Type() => ReactNode | - |
| renderReference | 自定义单个引用的渲染。 | Type(reference: Reference) => ReactNode | - |
| renderSkillItem | 自定义技能列表中的单项。参数的 id、role、aria-selected、className、onClick 与 onMouseEnter 需要展开到根元素上。 | Type(props: RenderSkillItemProps) => ReactNode | - |
| renderSuggestionItem | 自定义建议列表中的单项。参数的 id、role、aria-selected、className、onClick 与 onMouseEnter 需要展开到根元素上。 | Type(props: RenderSuggestionItemProps) => ReactNode | - |
| renderTemplate | 自定义模版面板的内容。参数为当前技能(仅显示模版按钮且未选中技能时为 undefined)与模版点击回调,回调传入的内容会替换输入框内容。 | Type(skill: Skill | undefined, onTemplateClick: (content: string) => void) => ReactNode | - |
| renderTopSlot | 自定义顶部区域的内容,每次渲染时调用。参数包含引用、上传文件、输入内容与删除回调。 | Type(props: RenderTopSlotProps) => ReactNode | - |
| renderUploadButton | 自定义底部操作区的上传按钮 UI,上传与粘贴上传仍由组件内部托管。 | Type(props: RenderUploadButtonProps) => ReactNode | - |
| round | 底部的配置区域和操作区域按钮是否为全圆角。 | boolean | true |
| sendHotKey | 发送输入内容的键盘快捷键。enter 表示单独按下 Enter 发送,Shift 与 Enter 同时按下仅换行;shift+enter 相反。 | 'enter' | 'shift+enter' | "enter" |
| showPlaceholderWhenSkillOnly | 仅选中技能(无其他内容)时是否显示占位符,开启后占位符显示在技能后方,默认不显示;只在创建编辑器时读取。 | boolean | - |
| showReference | 是否展示引用区域,用于配合 renderTopSlot 使用。 | boolean | true |
| showTemplateButton | 为 true 时始终展示模版按钮;未设置或为 false 时,由当前选中技能的 hasTemplate 决定是否展示。 | boolean | - |
| showUploadButton | 是否显示右侧上传按钮。设为 false 时粘贴上传仍然有效。 | boolean | true |
| showUploadFile | 是否展示上传文件区域,用于配合 renderTopSlot 使用。 | boolean | true |
| skillHotKey | 输入框为空时按下该键唤起技能面板,需要同时提供 skills。 | string | - |
| skills | 技能列表,每项的 icon 为 ReactNode。 | Skill[] | - |
| style | 根元素的样式。 | React.CSSProperties | - |
| suggestions | 建议列表。列表变化且不为空时显示建议面板,为空时隐藏。 | Suggestion[] | - |
| templatesCls | 模版面板的类名。 | string | - |
| templatesStyle | 模版面板的样式,与默认的最大高度合并。 | React.CSSProperties | - |
| topSlotPosition | 顶部区域相对于引用区域与上传文件区域的位置,可选 top、middle 或 bottom。 | 'top' | 'middle' | 'bottom' | "top" |
| transformer | 自定义扩展的转换规则,用于保证 onContentChange 得到的节点数据符合预期。 | TypeMap<string, (obj: any) => any> | - |
| uploadProps | 上传文件相关配置,取值同 Upload 的属性,不含 fileList 与 listType。 | TypeOmit<UploadProps, 'fileList' | 'listType'> | - |
| uploadTipProps | 上传按钮的提示,取值同 Tooltip 的属性,包裹上传按钮节点(含 renderUploadButton 或 uploadProps.children 给出的节点)。 | TooltipProps | - |
Methods
| 方法 | 说明 | 类型 |
|---|---|---|
| changeTemplateVisible | 切换模版面板的可见性。 | Type(visible: boolean) => void |
| deleteContent | 删除富文本中的某一项,删除依据是 content 中的 uniqueKey。 | Type(content: Content) => void |
| deleteUploadFile | 删除上传文件中的某一项。 | Type(item: Attachment) => void |
| focusEditor | 聚焦输入框,默认聚焦到末尾,也可传入 FocusPosition。 | Type(pos?: FocusPosition) => void |
| getEditor | 获取当前的 tiptap Editor 实例,编辑器创建前为 null。 | Type() => Editor | null |
| setContent | 设置输入框内容。 | Type(content: TiptapContent) => void |
| setContentWhileSaveTool | 保留已选技能的同时设置输入框内容。 | Type(content: string) => void |
className、style 与未声明的原生属性(如 id、aria-label)落在根元素 div 上。onFocus、onBlur、onPaste 是富文本输入区的回调,不落在根元素上。
Configure.Select、Configure.Button、Configure.RadioButton 由 getConfigureItem 生成,值由 Configure 接管:不接受 value,field 必填,initValue 设置初始值,onChange 收到该项的新值。
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| field | 配置项在 Configure 值中的 key,必填 | string | - |
| initValue | 初始值 | 与被包装组件的 value 一致 | - |
| onChange | 该项的值变化回调 | (value: unknown) => void | - |
Configure.Select
其余属性同 SelectProps
Configure.Button
其余属性同 ButtonProps;值为 boolean,点击时切换,onClick 收到切换后的值 (value: boolean) => void
Configure.RadioButton
其余属性同 RadioGroupProps
Configure.Mcp
Props
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| afterClose | 菜单关闭且离场动画结束后调用。 | Type() => void | - |
| afterOpen | 菜单打开且进场动画结束后调用;没有进场动画时打开后即调用;进场结束前被关闭的打开不调用。 | Type() => void | - |
| aria-label | 弹出层 role 为 dialog 时的 aria-label。 | string | - |
| aria-labelledby | 弹出层 role 为 dialog 时的 aria-labelledby。 | string | - |
| arrowPointAtCenter | 箭头是否指向触发元素的中心;只在 showArrow 为 true 时生效。 | boolean | - |
| autoAdjustOverflow | 弹出层被遮挡时是否自动调整方向,默认 true。 | boolean | - |
| children | 替换标题下方内置的菜单,传入时不再由 options 生成菜单。 | React.ReactNode | - |
| className | 弹出层外层 .semi-dropdown-wrapper 的类名。 | string | - |
| clickToHide | 在弹出层内点击时是否自动关闭弹出层。 | boolean | - |
| clickTriggerToHide | 点击触发元素时是否关闭已打开的菜单。 | boolean | - |
| closeOnEsc | 在触发元素或弹出层按 Esc 是否关闭菜单,默认 true。trigger 为 custom 时只调用 onEscKeyDown,由使用方决定是否关闭。 | TooltipProps['closeOnEsc'] | - |
| condition | 是否允许触发显示;仅显式设为 false 时,hover、click、focus 等触发行为不生效,trigger 为 custom 时仍按 visible 显示。由 true 变为 false 时立即关闭已显示的菜单。 | boolean | - |
| content | Mcp 自行生成弹出内容,传入的 content 不生效。 | React.ReactNode | RenderContent | - |
| contentClassName | 下拉菜单容器 .semi-dropdown 的类名,可为字符串或数组。 | string | any[] | - |
| disableArrowKeyDown | 是否禁用在触发元素上按上下方向键把焦点移入弹出层。 | boolean | - |
| disableFocusListener | trigger 为 hover 时,是否不响应键盘聚焦打开菜单,默认 false。 | boolean | - |
| guardFocus | 焦点位于弹出层内时,Tab 是否在弹出层内循环,默认 false。 | boolean | - |
| keepDOM | 关闭时是否保留菜单的 DOM 而不销毁,默认 false。 | boolean | - |
| menu | Mcp 的菜单由 options 或 children 生成,传入的 menu 不生效。 | DropDownMenuItem[] | - |
| motion | 是否展示弹出层的进出场动画,默认 true。 | boolean | - |
| mouseEnterDelay | 指针移入触发元素后延迟显示的毫秒数,默认 50,仅 trigger 为 hover 或 focus 时生效。 | number | - |
| mouseLeaveDelay | 指针移出弹出层后延迟隐藏的毫秒数,仅 trigger 为 hover 或 focus 时生效,默认 100。 | number | - |
| num | 触发器与标题中显示的数量,不传时为 options 的数量。 | number | - |
| onClickOutSide | 按下触发元素与弹出层以外的区域时调用,参数是原生 MouseEvent。仅 trigger 为 click、contextMenu、custom 或开启 clickTriggerToHide 时生效。 | Type(e: MouseEvent) => void | - |
| onConfigureButtonClick | 点击配置按钮时调用。 | Type() => void | - |
| onEscKeyDown | 菜单打开期间按 Esc 时调用,参数是原生 KeyboardEvent。 | TooltipProps['onEscKeyDown'] | - |
| onOpen | 菜单显示、进场动画开始时调用,每次打开调用一次,早于 afterOpen;悬停打开后指针已离开而撤回的打开不调用。 | Type() => void | - |
| onVisibleChange | 菜单显示状态变化时调用。visible 受控时只报告用户交互发起的请求,使用方自己改变 visible 不调用。 | Type(visible: boolean) => void | - |
| options | MCP 选项列表,选项只含数据,图标为选项的 icon(ReactNode)。 | McpOption[] | [] |
| position | 弹出菜单相对触发元素的位置,常用 bottom、bottomLeft、bottomRight,默认 bottom,更多见 Tooltip 的位置。 | Position | - |
| prefixCls | 弹出层类名前缀,默认 semi-dropdown。 | string | - |
| preventScroll | 聚焦时是否阻止浏览器滚动到被聚焦元素,默认 false。 | boolean | - |
| render | Mcp 自行生成菜单内容,传入的 render 不生效。 | React.ReactNode | - |
| returnFocusOnClose | 菜单关闭后焦点是否回到触发元素,默认 true。 | boolean | - |
| role | 包裹菜单的弹层容器的 ARIA role,默认 presentation,菜单本身的 role 为 menu。 | string | - |
| selectionMode | 本层菜单项的选择方式,已选项由 active 标记:single 时菜单项为带 aria-checked 的 menuitemradio,multiple 时为带 aria-checked 的 menuitemcheckbox;未设置时 showTick 开启则为带 aria-checked 的 menuitemcheckbox,否则为 menuitem。只影响角色与状态,勾的显示仍由 showTick 决定;嵌套的子菜单不继承。 | DropdownSelectionMode | - |
| showArrow | 是否显示箭头,默认 false。 | boolean | React.ReactNode | - |
| showConfigure | 是否显示配置按钮。 | boolean | true |
| showTick | 是否在 active 的 Dropdown.Item 左侧展示表示选中的勾,默认 false;只作用于本 Dropdown 的菜单,嵌套的子菜单不继承。 | boolean | - |
| spacing | 弹出层与触发元素的距离,单位为像素;缺省时顶层菜单为 4,嵌套的子菜单为 2。 | number | { x: number; y: number; } | - |
| stopPropagation | 是否阻止弹出层上的 click、mousedown 与焦点进出事件冒泡,默认 false。 | boolean | - |
| style | 下拉菜单容器 .semi-dropdown 的样式。 | React.CSSProperties | - |
| trigger | 触发菜单显示的行为,可选 hover、focus、click、custom、contextMenu,默认 hover;custom 时显示与否完全由 visible 决定。 | Trigger | - |
| visible | 菜单是否显示。配合 trigger 为 custom 使用;传入后由该值决定显示,触发器、点击外部与 Esc 只通过 onVisibleChange 请求打开或关闭。 | boolean | - |
| wrapperClassName | 触发元素被 span 包裹时,该 span 的类名。 | string | - |
| wrapperId | 弹出层节点的 id,触发元素的 aria 属性指向它;缺省时自动生成。 | string | - |
| wrapWhenSpecial | 触发元素为禁用或加载中的 Button 等特殊触发器时,是否在外层包裹 span 以保证触发行为,默认 true。 | boolean | - |
className、style 与未声明的属性透传给内部的 Dropdown,落点同 Dropdown。
设计变量
| Sass 变量 | 值 | 用法 |
|---|---|---|
| $color-aiChatInput-border | var(--semi-color-border) | AIChatInput的边框颜色 |
| $color-aiChatInput_skill_item-bg-active | var(--semi-color-fill-0) | AIChatInput技能项悬停时的背景颜色 |
| $color-aiChatInput_suggestion_item-text | var(--semi-color-text-0) | AIChatInput建议项的文本颜色 |
| $color-aiChatInput_suggestion_item-bg-active | var(--semi-color-fill-0) | AIChatInput建议项悬停时的背景颜色 |
| $color-aiChatInput_references-text | var(--semi-color-text-2) | AIChatInput引用项的文本颜色 |
| $color-aiChatInput_reference-bg | var(--semi-color-fill-0) | AIChatInput引用项的背景色 |
| $color-aiChatInput_footer_send-text | var(--semi-white) | AIChatInput底部发送按钮的内容颜色 |
| $color-aiChatInput_footer_send-bg-default | var(--semi-color-primary) | AIChatInput底部发送按钮的背景颜色 - 默认 |
| $color-aiChatInput_footer_send-bg-hover | var(--semi-color-primary-hover) | AIChatInput底部发送按钮的背景颜色-悬停态 |
| $color-aiChatInput_footer_send-bg-active | var(--semi-color-primary-active) | AIChatInput底部发送按钮的背景颜色-激活态 |
| $color-aiChatInput_footer_send-bg-disabled | var(--semi-color-fill-2) | AIChatInput底部发送按钮的背景颜色-禁用态 |
| $color-aiChatInput_footer_upload-text | var(--semi-color-text-1) | AIChatInput底部上传按钮的内容颜色 |
| $color-aiChatInput_footer_upload-bg-default | transparent | AIChatInput底部上传按钮的背景颜色 - 默认 |
| $color-aiChatInput_footer_upload-bg-hover | var(--semi-color-fill-0) | AIChatInput底部上传按钮的背景颜色-悬停态 |
| $color-aiChatInput_footer_upload-bg-active | var(--semi-color-fill-1) | AIChatInput底部上传按钮的背景颜色-悬停态 |
| $color-aiChatInput_footer_configure_button-border-active | var(--semi-color-primary) | AIChatInput底部配置区域按钮的按钮边框颜色 - 激活态 |
| $color-aiChatInput_footer_configure_button-text | var(--semi-color-primary) | AIChatInput底部配置区域按钮的按钮文字颜色 - 激活态 |
| $color-aiChatInput_footer_configure_select-border | var(--semi-color-border) | AIChatInput底部配置区域下拉选择的按钮边框颜色 |
| $color-aiChatInput_footer_configure_radio_button_checked | var(--semi-color-text-1) | AIChatInput底部配置区域下拉选择的单选按钮选中时的背景颜色 |
| $color-aiChatInput_footer_configure_radio_button_checked-bg | var(--semi-color-bg-2) | AIChatInput底部配置区域下拉选择的单选按钮选中时的背景颜色 |
| $color-aiChatInput_footer-configure_mcp_header_title-text | var(--semi-color-text-2) | AIChatInput底部配置区域mcp配置下拉的顶部内容的标题颜色 |
| $color-aiChatInput_attachment-bg | var(--semi-color-fill-0) | AIChatInput附件项的背景颜色 |
| $color-aiChatInput_attachment_name-text | var(--semi-color-text-0) | AIChatInput附件项内容名称的文本颜色 |
| $color-aiChatInput_attachment_delete-bg | var(--semi-color-text-1) | AIChatInput附件项内容的删除按钮的背景颜色 |
| $color-aiChatInput_attachment_delete-icon | var(--semi-color-bg-0) | AIChatInput附件项内容的删除按钮的图标颜色 |
| $color-aiChatInput_attachment_scroll_button-bg | var(--semi-color-bg-2) | AIChatInput附件项滚动按钮的背景颜色 |
| $color-aiChatInput_attachment_scroll_button | var(--semi-color-text-2) | AIChatInput附件项滚动按钮的背景颜色 |
| $color-aiChatInput_rich_text-input_slot-bg | var(--semi-color-primary-light-default) | AIChatInput富文本编辑器输入插槽的背景颜色 |
| $color-aiChatInput_rich_text-input_slot-text | var(--semi-color-primary) | AIChatInput富文本编辑器输入插槽的文本颜色 |
| $color-aiChatInput_rich_text-input_slot_placeholder | var(--semi-color-primary-disabled) | AIChatInput富文本编辑器输入插槽占位符的文本颜色 |
| $color-aiChatInput_rich_text-select_slot-bg | var(--semi-color-primary-light-default) | AIChatInput富文本编辑器选择器插槽的背景颜色 |
| $color-aiChatInput_rich_text-select_selection-text | var(--semi-color-primary) | AIChatInput富文本编辑器选择器插槽的选择器文本颜色 |
| $color-aiChatInput_rich_text-select_slot_arrow | var(--semi-color-text-2) | AIChatInput富文本编辑器选择器插槽的下拉箭头颜色 |
| $color-aiChatInput_rich_text-skill_slot-text | var(--semi-color-primary) | AIChatInput富文本编辑器技能插槽的文本颜色 |
| $color-aiChatInput_rich_text-skill_slot-bg-hover | var(--semi-color-primary-light-default) | AIChatInput富文本编辑器技能插槽的背景色 - 悬停态 |
| $color-aiChatInput_rich_text-skill_slot_delete-bg | var(--semi-color-text-1) | AIChatInput富文本编辑器技能插槽的删除按钮的背景颜色 |
| $color-aiChatInput_rich_text-skill_slot_delete-text | var(--semi-color-bg-0) | AIChatInput富文本编辑器技能插槽的删除按钮的背景颜色 |
| $color-aiChatInput_ref_icon_word-bg | var(--semi-blue-4) | AIChatInput 引用/上传内容为文本文件时的图标背景色 |
| $color-aiChatInput_ref_icon_word-pdf | var(--semi-red-4) | AIChatInput 引用/上传内容为 pdf 时的图标背景色 |
| $color-aiChatInput_ref_icon_word-code | var(--semi-teal-5) | AIChatInput 引用/上传内容为代码的图标背景色 |
| $color-aiChatInput_ref_icon_word-excel | var(--semi-green-5) | AIChatInput 引用/上传内容为表格类型的图标背景色 |
| $color-aiChatInput_ref_icon_word-video | var(--semi-purple-4) | AIChatInput 引用/上传内容为视频类型的图标背景色 |
| $color-aiChatInput_ref_icon_word-audio | var(--semi-purple-4) | AIChatInput 引用/上传内容为音频类型的图标背景色 |
| $color-aiChatInput_ref_icon_word-unknown | var(--semi-grey-5) | AIChatInput 引用/上传内容为未知类型的图标背景色 |
| $color-aiChatInput_reference_delete-bg | var(--semi-color-fill-1) | AIChatInput引用项删除图标的背景色 - 悬停态 |
| $color-aiChatInput_footer_configure_select-text | var(--semi-color-text-1) | AIChatInput底部配置区域下拉选择的按钮文字颜色 |
| $color-aiChatInput_footer_configure_select-text | var(--semi-color-text-1) | AIChatInput底部配置区域下拉选择的按钮文字颜色 |
| $spacing-aiChatInput-padding | 20px | AIChatInput的外边距 |
| $spacing-aiChatInput_skill-paddingY | 4px | AIChatInput技能面板的垂直内边距 |
| $spacing-aiChatInput_skill-paddingX | 0px | AIChatInput技面板的水平内边距 |
| $spacing-aiChatInput_skill_item-columnGap | 8px | AIChatInput技能项内容的水平间距 |
| $spacing-aiChatInput_skill_item-paddingY | 8px | AIChatInput技能项的垂直内边距 |
| $spacing-aiChatInput_skill_item-paddingX | 20px | AIChatInput技能项的水平内边距 |
| $spacing-aiChatInput_suggestion_item-paddingY | 8px | AIChatInput建议项的垂直内边距 |
| $spacing-aiChatInput_suggestion_item-paddingX | 20px | AIChatInput建议项的水平内边距 |
| $spacing-aiChatInput_references-marginBottom | 8px | AIChatInput引用项的底部外边距 |
| $spacing-aiChatInput_references-columnGap | 4px | AIChatInput技能块内容的水平间距 |
| $spacing-aiChatInput_references-rowGap | 4px | AIChatInput技能块内容的垂直间距 |
| $spacing-aiChatInput_reference-paddingY | 8px | AIChatInput引用项的垂直内边距 |
| $spacing-aiChatInput_reference-paddingX | 12px | AIChatInput引用项的水平内边距 |
| $spacing-aiChatInput_reference-columnGap | 8px | AIChatInput引用项内容的水平间距 |
| $spacing-aiChatInput_footer-marginTop | 20px | AIChatInput引用项的顶部外边距 |
| $spacing-aiChatInput_footer_action-columnGap | 8px | AIChatInput底部右侧操作区域项目的水平间距 |
| $spacing-aiChatInput_footer_configure-columnGap | 8px | AIChatInput底部配置区域项目的水平间距 |
| $spacing-aiChatInput_footer_configure_radio_button-padding | 4px | AIChatInput底部配置区域下拉选择的单选按钮的内边距 |
| $spacing-aiChatInput_footer_configure_mcp_header-paddingTop | 8px | AIChatInput底部配置区域mcp配置下拉的顶部内容的上外边距 |
| $spacing-aiChatInput_footer_configure_mcp_header-paddingBottom | 0px | AIChatInput底部配置区域mcp配置下拉的顶部内容的下外边距 |
| $spacing-aiChatInput_footer_configure_mcp_header-paddingX | 16px | AIChatInput底部配置区域mcp配置下拉的顶部内容的水平外边距 |
| $spacing-aiChatInput_footer_configure_mcp_columnGap | 4px | AIChatInput底部配置区域mcp配置下拉的顶部内容的水平间距 |
| $spacing-aiChatInput_attachment-columnGap | 8px | AIChatInput附件项的水平间距 |
| $spacing-aiChatInput_attachment-padding | 8px | AIChatInput附件项的内间距 |
| $spacing-aiChatInput_attachment_content_size-columnGap | 4px | AIChatInput附件项类型和大小的间距 |
| $spacing-aiChatInput_attachment_scroll_wrapper-marginBottom | 8px | AIChatInput附件项滚动区域的底部外边距 |
| $spacing-aiChatInput_attachment_scroll_container | 8px | AIChatInput附件项滚动区域的内间距 |
| $spacing-aiChatInput_attachment_scroll_button_offsetX | 10px | AIChatInput附件项滚动按钮的和滚动区域的水平距离 |
| $spacing-aiChatInput_rich_text-input_slot-paddingY | 2px | AIChatInput富文本编辑器输入插槽的垂直内边距 |
| $spacing-aiChatInput_rich_text-input_slot-paddingX | 4px | AIChatInput富文本编辑器输入插槽的水平内边距 |
| $spacing-aiChatInput_rich_text-input_slot-marginY | 2px | AIChatInput富文本编辑器输入插槽的垂直内边距 |
| $spacing-aiChatInput_rich_text-input_slot-marginX | 2px | AIChatInput富文本编辑器输入插槽的水平内边距 |
| $spacing-aiChatInput_rich_text-select_slot-paddingX | 4px | AIChatInput富文本编辑器选择器插槽的水平内边距 |
| $spacing-aiChatInput_rich_text-select_slot-paddingY | 2px | AIChatInput富文本编辑器选择器插槽的垂直内边距 |
| $spacing-aiChatInput_rich_text-select_slot-marginX | 4px | AIChatInput富文本编辑器选择器插槽的垂直外边距 |
| $spacing-aiChatInput_rich_text-select_slot-marginY | 2px | AIChatInput富文本编辑器选择器插槽的水平外边距 |
| $spacing-aiChatInput_rich_text-select_selection-marginLeft | 0px | AIChatInput富文本编辑器选择器插槽的选择器文本的左侧外边距 |
| $spacing-aiChatInput_rich_text-skill_slot-paddingX | 4px | AIChatInput富文本编辑器技能插槽的水平内边距 |
| $spacing-aiChatInput_rich_text-skill_slot-paddingY | 0px | AIChatInput富文本编辑器技能插槽的垂直内边距 |
| $spacing-aiChatInput_rich_text-skill_slot-marginX | 2px | AIChatInput富文本编辑器技能插槽的水平外边距 |
| $spacing-aiChatInput_rich_text-skill_slot-marginY | 2px | AIChatInput富文本编辑器技能插槽的垂直外边距 |
| $spacing-aiChatInput_rich_text-skill_slot-top | 0px | AIChatInput富文本编辑器技能插槽删除按钮距离顶部距离 |
| $spacing-aiChatInput_rich_text-skill_slot-right | 0px | AIChatInput富文本编辑器技能插槽删除按钮距离右侧距离 |
| $spacing-aiChatInput_reference_icon-marginRight | 4px | AIChatInput 引用项的图标距离右侧距离 |
| $spacing-aiChatInput_references_delete-padding | 2px | AIChatInput引用项删除图标的内间距 |
| $width-aiChatInput-border | 1px | AIChatInput的边框宽度 |
| $width-aiChatInput_footer_action_button | 32px | AIChatInput底部右侧操作按钮的宽度 |
| $height-aiChatInput_footer_action_button | 32px | AIChatInput底部右侧操作按钮的高度 |
| $width-aiChatInput_footer_configure_button-border | 1px | AIChatInput底部配置区域按钮的边框宽度 |
| $width-aiChatInput_footer_configure_select-border | 1px | AIChatInput底部配置区域下拉选择的边框宽度 |
| $height-aiChatInput_footer_configure_mcp_header | 16px | AIChatInput底部配置区域mcp配置下拉的顶部内容的高度 |
| $width-aiChatInput_attachment | 240px | AIChatInput附件项的宽度(含内边距) |
| $height-aiChatInput_attachment_left | 36px | AIChatInput附件项左侧图标/图片的宽高 |
| $width-aiChatInput_attachment-content | 180px | AIChatInput附件项内容的宽度 |
| $height-aiChatInput_attachment-content | 20px | AIChatInput附件项内容的高度 |
| $width-aiChatInput_attachment_delete | 16px | AIChatInput附件项内容的删除按钮的宽度 |
| $width-aiChatInput_attachment_scroll_button | 16px | AIChatInput附件项滚动按钮的宽度 |
| $width-aiChatInput_attachment_scroll_button_icon | 12px | AIChatInput附件项滚动按钮的图标大小 |
| $height-aiChatInput_rich_text-select-slot | 24px | AIChatInput富文本编辑器选择器插槽的高度 |
| $width-aiChatInput_rich_text-select_slot_arrow | 16px | AIChatInput富文本编辑器选择器插槽的下拉箭头宽度 |
| $width-aiChatInput_rich_text-select_slot_delete | 12px | AIChatInput富文本编辑器选择器插槽的删除按钮的宽高 |
| $width-aiChatInput_rich_text-select_slot_delete_icon | 8px | AIChatInput富文本编辑器选择器插槽的删除按钮icon的宽高 |
| $width-aiChatInput_rich_text-input_slot | 2px | AIChatInput富文本编辑器输入插槽的最小宽度 |
| $width-aiChatInput_reference_icon | 16px | AIChatInput 引用项的图标宽度 |
| $radius-aiChatInput | 12px | AIChatInput输入框的圆角 |
| $radius-aiChatInput_skill | 8px | AIChatInput技能块的圆角 |
| $radius-aiChatInput_reference | 6px | AIChatInput引用项的圆角 |
| $radius-aiChatInput_footer_round | 9999px | AIChatInput底部操作项目全圆角模式的圆角 |
| $radius-aiChatInput_footer_action_button | 8px | AIChatInput底部右侧操作按钮的圆角 |
| $radius-aiChatInput_attachment | 6px | AIChatInput附件项的圆角 |
| $radius-aiChatInput_rich_text-input_slot | 4px | AIChatInput富文本编辑器输入插槽的圆角 |
| $radius-aiChatInput_rich_text-select_slot | 4px | AIChatInput富文本编辑器选择器插槽的圆角 |
| $radius-aiChatInput_rich_text-skill_slot | 4px | AIChatInput富文本编辑器技能插槽的圆角 |
| $radius-aiChatInput_reference_icon | 2px | AIChatInput 引用项的图标圆角 |
| $radius-aiChatInput_suggestion_item | 6px | AIChatInput 建议项的圆角 |
| $font-aiChatInput_footer_configure_select-fontWeight | 600 | AIChatInput底部配置区域下拉选择的字体粗细 |
| $font-aiChatInput_attachment_content_name-fontWeight | 600 | AIChatInput附件项内容的名称字体粗细 |
| $font-aiChatInput_attachment_content_delete-fontSize | 12px | AIChatInput附件项内容的删除按钮的图标大小 |
| $font-aiChatInput_rich_text-fontSize | $font-size-regular | AIChatInput富文本编辑器文字大小 |
| $font-aiChatInput_rich_text-lineHeight | 24px | AIChatInput富文本编辑器文本行高 |
| $font-aiChatInput_rich_text-input_slot-fontSize | $font-size-regular | AIChatInput富文本编辑器输入插槽文字大小 |
| $font-aiChatInput_rich_text-input_slot-lineHeight | 20px | AIChatInput富文本编辑器输入框插槽文本行高 |
| $font-aiChatInput_rich_text-input_slot-fontWeight | 600 | AIChatInput富文本编辑器输入插槽文本字重 |
| $font-aiChatInput_rich_text-skill_slot-fontWeight | 600 | AIChatInput富文本编辑器技能插槽文本字重 |
| $font-aiChatInput_footer_configure_radio_button-fontSize | 14px | AIChatInput底部配置区域下拉选择的单选按钮的文字大小 |
| $z-aiChatInput_attachment_scroll_button | 10 | AIChatInput附件项滚动按钮的z-index |
| $z-aiChatInput_rich_text-input_slot_placeholder | 1 | AIChatInput富文本编辑器输入插槽占位符的z-index |
| $shadow-aiChatInput_attachment_scroll_button | var(--semi-shadow-elevated) | AIChatInput附件项滚动按钮的阴影 |