Usage Scenarios
In AI chat scenarios, users can use AIChatInput to achieve rich text input, uploading, quoting, suggestions, templates, feature configuration, and rich custom display.
AIChatInput's rich text input is based on tiptap (https://tiptap.dev/docs/editor/getting-started/overview), a modern rich text editor development framework that supports mainstream front-end frameworks such as React and Vue, and boasts strong customizability and extensibility. Its componentization capabilities are excellent, performance is high, it has many built-in commonly used extensions, and it supports user-defined nodes, commands, plugins, and menus, enabling flexible adaptation and expansion of rich text input capabilities in complex AI scenarios. Semi's AIChatInput component encapsulates tiptap, allowing developers to use it out of the box or extend it as needed according to business requirements.
Demos
How to import
import { AIChatInput } from '@fibrous/semi-ai-react';
Basic Usage
Supports text input and file upload. You can configure the following parameters as needed:
uploadProps: Configure parameters related to file upload. See UploadPropsonUploadChange: Callback when file upload changes- When deleting uploaded files,
uploadProps.onRemovewill be triggered, anduploadProps.beforeRemovewill be respected (Promise supported) placeholder: Placeholder for the input boxdefaultContent: Default content for the input boxonContentChange: Callback when the content of the input box changes; the parameter is the current content
Code
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} />;
}Message Sending
When there is content in the input box (text entry or uploaded content), sending messages is allowed. Clicking the send message button triggers the onMessageSend callback; the argument is the input content, including text, reference content, uploaded files, and configuration area content. References do not count towards whether sending is allowed; canSend can also set it directly.
You can manage generating status with generating. If generating is true, AIChatInput will show a stop-generating button instead of the send button and clear the input area as well as uploaded files. References require manual handling.
Clicking the stop button triggers onStopGenerate, where you can handle logic such as setting generating to false.
Code
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}
/>
);
}Rich Text Input
AIChatInput uses tiptap for its rich text editor. You can enter text, use built-in extensions (including input-slot, select-slot, skill-slot), or extend with your own.
input-slot: Supports text input and placeholder display.select-slot: Supports in-box option selection with string options.skill-slot: For skill display blocks.
You can set input content with the setContent ref method and focus the editor with focusEditor.
Code
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} />
</>
);
}Reference
You can pass in references via the references, which will display at the top of the input box.
renderReference: Custom renderer for an individual reference.onReferenceDelete: Callback for deleting a reference.onReferenceClick: Callback for clicking a reference.
Code
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} />;
}Configuration Area
You can configure options such as model parameters, web search, and critical thinking through the configuration area, or display/view MCP tools.
renderConfigureArea: Custom renderer for config area buttons.- Use
Configurecomponents such asSelect,Button,Mcp,RadioButton, etc.
The Configure component manages the state and provides a callback via onConfigureChange (make sure to set the unique field). For initial values, use initValue.
You can also use getConfigureItem to adapt your custom components.
Code
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} />;
}You can extend any custom component for configuration using getConfigureItem.
function getConfigureItem<P, VK extends keyof P = 'value', CK extends keyof P = 'onChange'>(
component: React.ComponentType<P>,
opts?: {
// the prop that receives the value, value by default
valueKey?: VK;
// the prop called when the value changes, onChange by default
onKeyChangeFnName?: CK;
// where the value sits in the change callback's first argument, the argument itself by default
valuePath?: string;
className?: string;
defaultProps?: Partial<P>;
}
): React.ComponentType<
Omit<P, VK | CK | 'onChange' | 'className'> & {
// the key of this item in the Configure value, required
field: string;
initValue?: P[VK];
onChange?: (value: unknown) => void;
className?: string;
}
>;
The returned component checks its props against the wrapped component's props: field is required, and initValue has the type of the prop named by valueKey. onChange belongs to the item and receives its new value; the wrapped component's own onChange is not accepted.
Demo:
Code
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} />;
}Action Area
The lower right corner is the action area. Use renderActionArea to customize buttons (e.g. for deleting or other operations). menuItem holds the upload node (a hidden Upload when showUploadButton is false; it must still be rendered for pasted files to upload) and the send or stop button, in that order.
interface ActionAreaProps {
menuItem: ReactNode[];
className: string;
}
Code
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} />;
}Custom Upload Button
By default, an upload button is rendered on the left side of the footer action area. Use renderUploadButton for UI-only customization (e.g. icon-only button, tooltip, etc.).
Note: This does not affect upload / paste-upload behavior (Upload is still managed internally). openFileDialog triggers the internal Upload file chooser.
Code
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}
/>
);
}Button Shape
You can use the round API to configure the button shape at the bottom. The default is true (rounded). Set it to false for square buttons.
Code
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
Configure suggestions with the suggestions API. This works similarly to the AutoComplete component. Users can dynamically show suggestions based on input.
Use up/down keys to navigate suggestions. Pressing ESC or clicking outside the suggestion/input area will close the suggestions.
You can customize rendering using renderSuggestionItem; spread id, role, aria-selected and the event handlers of its argument onto the root element.
Code
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 & Templates
Configure a skill list with skills, and use skillHotKey to set the shortcut for skill panel.
skillssample format:
interface Skill {
label?: string;
value?: string;
icon?: React.ReactNode;
// If this skill has a template, set hasTemplate to true, affects the display of template display buttons
hasTemplate?: boolean;
}
Because templates can be displayed in a variety of ways, we don't provide a default display method. Users can customize the template display through the renderTemplate API. The template panel can be displayed and closed by clicking the template button.
renderTemplate?: (
// undefined when the template button is shown without a selected skill
skill: Skill | undefined,
// called when a template is clicked, which replaces the input content with the template content
onTemplateClick: (content: string) => void
) => ReactNode;
Usage examples are as follows:
Code
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} />;
}Custom Top Slot
Users can customize the top rendering area using the renderTopSlot API, rendering references, uploaded content, and configuration items. This can be combined with the showReference and showUploadFile APIs to control whether references and uploaded files are displayed. Additionally, the topSlotPosition API allows you to configure the relative position of customized rendered content relative to the reference and upload display areas.
interface TopSlotProps {
references: Reference[];
attachments: Attachment[];
// User input into the input box
content: Content[];
handleUploadFileDelete: (attachment: Attachment) => void;
handleReferenceDelete: (reference: Reference) => void;
}
Usage examples are as follows:
Code
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="自定义渲染顶部内容,可用于渲染上传内容、引用内容" />;
}Custom Extensions
Rich text areas support custom extensions. For implementation details, see Tiptap Custom Extensions. Custom extensions can be added to the AIChatInput component using the extensions API.
If you add a custom extension, you must configure the corresponding transformation rules in transformer to ensure that the data returned in onContentChange matches your expectations.
When adding a custom extension, please note the following:
- Please add the
isCustomSlotproperty to your custom extension. This property is related to the cursor height before and after the custom extension. - Since
AIChatInputusesEnteras the send hotkey, if your custom extension usesEnteras a shortcut, you need to manually configureSemiAIChatInput.allowHotKeySendineditor.storageto indicate whether the hotkey should be used by AIChatInput for sending, in order to avoid hotkey conflicts. WhensendHotKeyisshift+enter, a plainEnterno longer breaks the line whileallowHotKeySendisfalse; it is left to the custom extension.
An example of a custom extension definition and related notes is as follows:
Code
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 Reference
When the root element carries aria-label or aria-labelledby and no role is passed, role="group" is added. The thumbnail on an attachment card is decorative (empty alt); the file name comes from the visible text in the card.
AIChatInput
Props
| Property | Description | Type | Default |
|---|---|---|---|
| canSend | Whether sending is allowed. When unset, it depends on the input content and the uploaded files; references do not count. | boolean | - |
| className | Class name of the root element. | string | - |
| clearContentOnGenerating | Whether the input content and attachments are cleared when generating turns from false to true. Set it to false to keep the input (for non-conversational scenes such as an infinite canvas). | boolean | true |
| defaultContent | Default content of the input, an html string or JSON like Tiptap's Content. Read once, when the editor is created. | TiptapContent | - |
| dropdownMatchTriggerWidth | Whether the dropdown layer has the same width as the input. | boolean | true |
| extensions | Custom tiptap extensions. Read once, when the editor is created; later changes have no effect. | Extensions | - |
| generating | Whether a response is being generated. While true, the send button becomes the stop button. | boolean | false |
| immediatelyRender | Passed to the tiptap editor as its immediatelyRender option. When false, the editor is created after mount, which works with server rendering; when true, it is created during the first render, for client-only rendering. | boolean | false |
| keepSkillAfterSend | Whether the selected skill is kept after a message is sent. | boolean | false |
| onBlur | Called when focus leaves the editor area, with the focus event. | Type(event: React.FocusEvent) => void | - |
| onConfigureChange | Called when a configure value changes. changedValue is undefined when an item is removed. | Type(value: LeftMenuChangeProps, changedValue?: LeftMenuChangeProps) => void | - |
| onContentChange | Called when the input content changes, with the current content. | Type(contents: Content[]) => void | - |
| onFocus | Called when the editor area gains focus, with the focus event. | Type(event: React.FocusEvent) => void | - |
| onMessageSend | Called when a message is sent, by the send button or the send shortcut, with the references, uploaded files, input content and configure values. | Type(props: MessageContent) => void | - |
| onPaste | Called when content is pasted into the editor. It does not prevent the default paste; read the content from event.clipboardData. | Type(event: ReactClipboardEvent<HTMLDivElement>) => void | - |
| onReferenceClick | Called when a reference is clicked, with that reference. | Type(reference: Reference) => void | - |
| onReferenceDelete | Called when a reference is deleted, with the deleted reference. The caller owns the references list and removes it in the callback. | Type(reference: Reference) => void | - |
| onSkillChange | Called when the skill changes; the argument is undefined when the skill is removed. | Type(skill?: Skill) => void | - |
| onStopGenerate | Called when the stop button is clicked. | Type() => void | - |
| onUploadChange | Called when the uploaded file list changes. | Type(props: OnChangeProps) => void | - |
| placeholder | Placeholder of the input: a string, or a function that returns a string for a node. | PlaceholderProps | - |
| popoverProps | Props of the dropdown layer. The className is merged with the internal class names and the style with the panel width. | PopoverProps | - |
| ref | Reference to the imperative methods, typed as AIChatInputHandle. It does not point to the root element. | TypeReact.Ref<AIChatInputHandle> | - |
| references | References shown with the input. | Reference[] | - |
| renderActionArea | Customizes the action area at the bottom. menuItem lists, in order, the upload node (a hidden Upload when showUploadButton is false, which must still render for paste upload) and the send or stop button; className is the class name of the area. | Type(props: ActionAreaProps) => ReactNode | - |
| renderConfigureArea | Customizes the configure area at the bottom; returns the configure items. | Type() => ReactNode | - |
| renderReference | Customizes how a single reference is rendered. | Type(reference: Reference) => ReactNode | - |
| renderSkillItem | Customizes one item of the skill list. The id, role, aria-selected, className, onClick and onMouseEnter of the argument must be spread onto the root element. | Type(props: RenderSkillItemProps) => ReactNode | - |
| renderSuggestionItem | Customizes one item of the suggestion list. The id, role, aria-selected, className, onClick and onMouseEnter of the argument must be spread onto the root element. | Type(props: RenderSuggestionItemProps) => ReactNode | - |
| renderTemplate | Customizes the template panel. It receives the current skill (undefined when only the template button is shown without a selected skill) and the template click callback, whose content replaces the input content. | Type(skill: Skill | undefined, onTemplateClick: (content: string) => void) => ReactNode | - |
| renderTopSlot | Customizes the top area; it is called on each render. The argument carries references, uploaded files, the input content and the delete callbacks. | Type(props: RenderTopSlotProps) => ReactNode | - |
| renderUploadButton | Customizes the upload button UI of the footer action area; the upload and paste upload stay inside the component. | Type(props: RenderUploadButtonProps) => ReactNode | - |
| round | Whether the buttons of the footer configure and action areas are fully rounded. | boolean | true |
| sendHotKey | Keyboard shortcut that sends the input. enter sends on Enter alone, and Shift with Enter only starts a new line; shift+enter does the opposite. | 'enter' | 'shift+enter' | "enter" |
| showPlaceholderWhenSkillOnly | Whether the placeholder shows when only a skill is selected, after the skill. Off by default. Read once, when the editor is created. | boolean | - |
| showReference | Whether the reference area is shown; use it together with renderTopSlot. | boolean | true |
| showTemplateButton | When true, the template button is always shown. When unset or false, the hasTemplate of the selected skill decides. | boolean | - |
| showUploadButton | Whether the upload button on the right is shown. When false, paste upload still works. | boolean | true |
| showUploadFile | Whether the uploaded file area is shown; use it together with renderTopSlot. | boolean | true |
| skillHotKey | Key that opens the skill panel when the input is empty; it needs skills as well. | string | - |
| skills | Skill list. The icon of each skill is a ReactNode. | Skill[] | - |
| style | Style of the root element. | React.CSSProperties | - |
| suggestions | Suggestion list. The suggestion panel shows when the list changes to a non-empty one and hides when it is empty. | Suggestion[] | - |
| templatesCls | Class name of the template panel. | string | - |
| templatesStyle | Style of the template panel, merged over its default max height. | React.CSSProperties | - |
| topSlotPosition | Position of the top area relative to the reference area and the uploaded file area: top, middle or bottom. | 'top' | 'middle' | 'bottom' | "top" |
| transformer | Conversion rules of custom extensions, so that the node data received by onContentChange matches what is expected. | TypeMap<string, (obj: any) => any> | - |
| uploadProps | Upload settings, the Upload props without fileList and listType. | TypeOmit<UploadProps, 'fileList' | 'listType'> | - |
| uploadTipProps | Tip of the upload button, with the Tooltip props. It wraps the upload button node, including the node given by renderUploadButton or uploadProps.children. | TooltipProps | - |
Methods
| Method | Description | Type |
|---|---|---|
| changeTemplateVisible | Shows or hides the template panel. | Type(visible: boolean) => void |
| deleteContent | Removes one item from the editor, matched by the uniqueKey of the content. | Type(content: Content) => void |
| deleteUploadFile | Removes one uploaded file. | Type(item: Attachment) => void |
| focusEditor | Focuses the editor. It focuses the end by default, or pass a FocusPosition. | Type(pos?: FocusPosition) => void |
| getEditor | Returns the tiptap Editor instance, or null before the editor is created. | Type() => Editor | null |
| setContent | Sets the input content. | Type(content: TiptapContent) => void |
| setContentWhileSaveTool | Sets the input content while keeping the selected skill. | Type(content: string) => void |
className, style and undeclared native attributes (such as id and aria-label) land on the root div. onFocus, onBlur and onPaste are callbacks of the rich text input and do not land on the root element.
Configure.Select, Configure.Button and Configure.RadioButton are made with getConfigureItem, and Configure owns their value: they take no value, field is required, initValue sets the initial value, and onChange receives the item's new value.
| Property | Description | Type | Default |
|---|---|---|---|
| field | The key of this item in the Configure value, required | string | - |
| initValue | Initial value | Same as the wrapped component's value | - |
| onChange | Called when the item's value changes | (value: unknown) => void | - |
Configure.Select
Other props are the same as SelectProps
Configure.Button
Other props are the same as ButtonProps. The value is a boolean toggled on click, and onClick receives the toggled value: (value: boolean) => void
Configure.RadioButton
Other props are the same as RadioGroupProps
Configure.Mcp
Props
| Property | Description | Type | Default |
|---|---|---|---|
| afterClose | Called after the menu has closed and its leave animation has ended. | Type() => void | - |
| afterOpen | Called after the menu has opened and its enter animation has ended, or right after opening when there is no enter animation. An opening closed before the enter animation ends does not call it. | Type() => void | - |
| aria-label | aria-label of the popup when its role is dialog. | string | - |
| aria-labelledby | aria-labelledby of the popup when its role is dialog. | string | - |
| arrowPointAtCenter | Whether the arrow points at the center of the trigger. Takes effect only when showArrow is true. | boolean | - |
| autoAdjustOverflow | Whether the menu flips to another side when it would be clipped. Defaults to true. | boolean | - |
| children | Replaces the menu under the header. When given, the menu is no longer built from options. | React.ReactNode | - |
| className | Class name of the popup's outer .semi-dropdown-wrapper element. | string | - |
| clickToHide | Whether a click inside the popup closes it. | boolean | - |
| clickTriggerToHide | Whether a click on the trigger closes an open menu. | boolean | - |
| closeOnEsc | Whether Esc on the trigger or in the popup closes the menu. Defaults to true. With trigger custom it only calls onEscKeyDown, and the caller decides whether to close. | TooltipProps['closeOnEsc'] | - |
| condition | Whether the trigger may show the menu. Only an explicit false disables the hover, click and focus triggers; with trigger custom, visible still shows the menu. Changing it from true to false closes an open menu at once. | boolean | - |
| content | Mcp builds the popup content itself, so a content passed here has no effect. | React.ReactNode | RenderContent | - |
| contentClassName | Class name of the menu container .semi-dropdown, a string or an array. | string | any[] | - |
| disableArrowKeyDown | Whether pressing the Up or Down arrow key on the trigger is kept from moving focus into the popup. | boolean | - |
| disableFocusListener | Whether keyboard focus is ignored when trigger is hover. Defaults to false. | boolean | - |
| guardFocus | Whether Tab cycles inside the popup while focus is in it. Defaults to false. | boolean | - |
| keepDOM | Whether the menu's DOM is kept instead of destroyed when it closes. Defaults to false. | boolean | - |
| menu | The menu of Mcp is built from options or children, so a menu passed here has no effect. | DropDownMenuItem[] | - |
| motion | Whether the popup plays its enter and leave animations. Defaults to true. | boolean | - |
| mouseEnterDelay | Milliseconds before the menu shows after the pointer enters the trigger. Defaults to 50. Applies to the hover and focus triggers. | number | - |
| mouseLeaveDelay | Milliseconds before the menu hides after the pointer leaves the popup. Defaults to 100. Applies to the hover and focus triggers. | number | - |
| num | Number shown in the trigger and the header. Defaults to the length of options. | number | - |
| onClickOutSide | Called when the mouse is pressed outside the trigger and the popup, with the native MouseEvent. Takes effect only for trigger click, contextMenu or custom, or with clickTriggerToHide. | Type(e: MouseEvent) => void | - |
| onConfigureButtonClick | Called when the configure button is clicked. | Type() => void | - |
| onEscKeyDown | Called when Esc is pressed while the menu is open, with the native KeyboardEvent. | TooltipProps['onEscKeyDown'] | - |
| onOpen | Called once per opening when the menu is shown and its enter animation starts, before afterOpen. An opening withdrawn because the pointer already left does not call it. | Type() => void | - |
| onVisibleChange | Called when the visibility of the menu changes. With a controlled visible it reports only requests from user interaction, not changes the caller makes to visible. | Type(visible: boolean) => void | - |
| options | MCP option list. Options hold data only; the icon of an option is a ReactNode. | McpOption[] | [] |
| position | Position of the menu relative to the trigger. Common values are bottom, bottomLeft and bottomRight; the default is bottom. See the Tooltip positions for more. | Position | - |
| prefixCls | Class prefix of the popup, semi-dropdown by default. | string | - |
| preventScroll | Whether focusing an element avoids scrolling it into view. Defaults to false. | boolean | - |
| render | Mcp builds the menu content itself, so a render passed here has no effect. | React.ReactNode | - |
| returnFocusOnClose | Whether focus returns to the trigger after the menu closes. Defaults to true. | boolean | - |
| role | ARIA role of the popup container around the menu, presentation by default. The menu itself has role menu. | string | - |
| selectionMode | How the items of this menu are selected; active marks the selected ones. With single the items are menuitemradio with aria-checked, with multiple menuitemcheckbox with aria-checked; unset, they are menuitemcheckbox with aria-checked when showTick is on, otherwise menuitem. It sets roles and states only: showTick alone shows the tick. Nested submenus do not inherit it. | DropdownSelectionMode | - |
| showArrow | Whether the arrow is shown. Defaults to false. | boolean | React.ReactNode | - |
| showConfigure | Whether the configure button is shown. | boolean | true |
| showTick | Whether an active Dropdown.Item shows a tick on its leading side. Defaults to false. It applies to the menu of this Dropdown only; nested submenus do not inherit it. | boolean | - |
| spacing | Distance in pixels between the popup and the trigger. Defaults to 4 for a top-level menu and 2 for a nested submenu. | number | { x: number; y: number; } | - |
| stopPropagation | Whether click, mousedown and focus in/out events on the popup stop propagating. Defaults to false. | boolean | - |
| style | Style of the menu container .semi-dropdown. | React.CSSProperties | - |
| trigger | What shows the menu: hover, focus, click, custom or contextMenu. Defaults to hover. With custom, visible alone decides. | Trigger | - |
| visible | Whether the menu is shown. Used with trigger custom. When given, this value decides, and the trigger, a click outside and Esc only request a change through onVisibleChange. | boolean | - |
| wrapperClassName | Class name of the span that wraps the trigger when a wrapper is needed. | string | - |
| wrapperId | id of the popup element, referenced by the trigger's ARIA attributes. Generated when omitted. | string | - |
| wrapWhenSpecial | Whether a special trigger, such as a disabled or loading Button, is wrapped in a span so that it still triggers the menu. Defaults to true. | boolean | - |
className, style and undeclared attributes fall through to the inner Dropdown and land where they do on Dropdown.
Design Tokens
| Sass variable | Value | Usage |
|---|---|---|
| $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附件项滚动按钮的阴影 |