--- localeCode: en-US order: 99 category: Ai title: AIChatInput icon: doc-aiInput width: 60% brief: Input box used in AI chat scenarios showNew: true --- ## 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 ```jsx import import { AIChatInput } from '@douyinfe/semi-ui'; ``` ### Basic Usage Supports text input and file upload. You can configure the following parameters as needed: - `uploadProps`: Configure parameters related to file upload. See [UploadProps](/en-US/plus/upload#API) - `onUploadChange`: Callback when file upload changes - `placeholder`: Placeholder for the input box - `defaultContent`: Default content for the input box - `onContentChange`: Callback when the content of the input box changes; the parameter is the current content ```jsx live=true dir="column" noInline=true import React from 'react'; import { AIChatInput } from '@douyinfe/semi-ui'; const uploadProps = { action: "https://api.semi.design/upload" }; const outerStyle = { margin: 12 }; function Basic() { const onContentChange = useCallback((content) => { console.log('onContentChange', content); }, []); const onUploadChange = useCallback((fileList) => { console.log('onUploadChange', fileList); }, []); return ( ); }; render(); ``` ### Message Sending When there is content in the input box (including text entry, uploaded content, [reference content](/en-US/plus/aiChatInput#Reference)), 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. 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`. ```jsx live=true dir="column" noInline=true import React from 'react'; import { AIChatInput } from '@douyinfe/semi-ui'; const uploadProps = { action: "https://api.semi.design/upload", defaultFileList: [ { uid: '1', name: 'dy.jpeg', status: 'success', size: '130kb', url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/dy.png', }, { uid: '5', name: 'resso.jpeg', percent: 50, size: '222kb', url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/Resso.png', } ], }; const outerStyle = { margin: 12 }; const reference = [ { id: '1', type: 'text', content: 'Test text: This is a long text repeated many times for demonstration purposes...' } ]; function SendMessageAndStopGenerate() { const [references, setReferences] = useState(reference); const [generating, setGenerating] = useState(false); const onContentChange = useCallback((content) => { console.log('onContentChange', content); }, []); const onUploadChange = useCallback((fileList) => { console.log('onUploadChange', fileList); }, []); const toggleGenerate = useCallback((props) => { setGenerating(value => !value); }, []); const onMessageSend = useCallback((content) => { toggleGenerate(); setReferences([]); }, []); const handleReferenceDelete = useCallback((item) => { setReferences((references) => { const newReference = references.filter((ref) => ref.id !== item.id); return newReference; }); }, []); return ( ); }; render(); ``` ### Rich Text Input AIChatInput uses [tiptap](https://tiptap.dev/docs/editor/getting-started/overview) 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`. ```jsx live=true dir="column" noInline=true import React, { useRef, useCallback } from 'react'; import { AIChatInput } from '@douyinfe/semi-ui'; const uploadProps = { action: "https://api.semi.design/upload" }; const outerStyle = { margin: 12 }; const temp = { 'input-slot': 'I am an engineer', 'select-slot': 'I am a , please help me complete...', 'skill-slot': ' Please help me complete...' }; function RichTextExample() { const [activeIndex, setActiveIndex] = useState(0); const ref = useRef(); const setTemplate = useCallback((event) => { const index = Number(event.target.dataset.index); setActiveIndex(index); const content = Object.values(temp)[index]; if (ref.current) { ref.current.setContent(content); ref.current.focusEditor(); } }, [ref]); return (<>
{Object.keys(temp).map((item, index) => (
{item}
))}
); }; render(); ``` ### 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. ```jsx live=true dir="column" noInline=true import React from 'react'; import { AIChatInput } from '@douyinfe/semi-ui'; const uploadProps = { action: "https://api.semi.design/upload" }; const outerStyle = { margin: 12 }; const referenceTemp = [ { id: '1', type: 'text', content: 'Sample text, repeated to demonstrate a long text.' }, { id: '2', name: 'FeishuDoc.docx' }, { id: '3', name: 'FeishuDoc.pdf' }, { id: '4', name: 'Music.mp4' }, { id: '5', name: 'Image.jpeg', url: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/root-web-sites/Resso.png' }, { id: '6', name: 'code.json' } ]; function Reference() { const [references, setReferences] = useState(referenceTemp); const handleReferenceDelete = useCallback((item) => { const newReference = references.filter((ref) => ref.id !== item.id); setReferences(newReference); }, [references]); const handleReferenceClick = useCallback((item) => { console.log('Clicked reference', item); }, []); return ( ); }; render(); ``` ### 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 `Configure` components such as `Select`, `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. ```jsx live=true dir="column" noInline=true import React from 'react'; import { AIChatInput } from '@douyinfe/semi-ui'; import { IconFixedStroked, IconBookOpenStroked, IconFeishuLogo, IconGit, IconFigma } from '@douyinfe/semi-icons'; 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: , label: "FeishuDoc", value: "feishu" }, { icon: , label: "Github Mcp", value: "github" }, { icon: , label: "IconFigma Mcp", value: "IconFigma" } ]; const radioButtonProps = [ { label: 'Fast', value: 'fast' }, { label: 'Think', value: 'think' }, { label: 'Super', value: 'super' } ]; function ConfigureButton() { const onConfigureButtonClick = useCallback(() => { console.log('onConfigureButtonClick'); }, []); const renderLeftMenu = useCallback(() => (<> } field="onlineSearch">Web search ), []); const onConfigureChange = useCallback((value, changedValue) => { console.log('onConfigureChange', value, changedValue); }, []); return ( ); }; render(); ``` You can extend any custom component for configuration using `getConfigureItem`. ```ts function getConfigureItem( component: React.ReactElement, opts: { valueKey?: string; onKeyChangeFnName?: string; valuePath?: string; className?: string; defaultProps?: Record } ) ``` Demo: ```jsx live=true dir="column" noInline=true import React, { useCallback } from 'react'; import { Cascader, AIChatInput, getConfigureItem } from '@douyinfe/semi-ui'; 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) => ; const CustomCascader = getConfigureItem(myCascader, { className: 'aiChatInput-cascader-configure' }); class CustomConfigure extends React.Component { constructor(props) { super(props); this.renderLeftMenu = this.renderLeftMenu.bind(this); this.onConfigureChange = this.onConfigureChange.bind(this); } renderLeftMenu() { return ; } onConfigureChange(value, changedValue) { console.log('onConfigureChange', value, changedValue); } render() { return (); }; } render(); ``` ### Action Area The lower right corner is the action area. Use `renderActionArea` to customize buttons (e.g. for deleting or other operations). ```jsx live=true dir="column" noInline=true import React from 'react'; import { AIChatInput, Divider, Button } from '@douyinfe/semi-ui'; import { IconDeleteStroked } from '@douyinfe/semi-icons'; const uploadProps = { action: "https://api.semi.design/upload" }; const outerStyle = { margin: 12 }; function ActionArea() { const renderActionArea = useCallback((props) => (
{props.menuItem}
), []); return ( ); }; render(); ``` ### 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. ```jsx live=true dir="column" noInline=true import React from 'react'; import { AIChatInput, RadioGroup, Radio } from '@douyinfe/semi-ui'; import { IconFixedStroked, IconBookOpenStroked, IconFeishuLogo, IconGit, IconFigma } from '@douyinfe/semi-icons'; 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: , label: "FeishuDoc", value: "feishu" }, { icon: , label: "Github Mcp", value: "github" }, { icon: , label: "IconFigma Mcp", value: "IconFigma" } ]; const radioButtonProps = [ { label: 'Fast', value: 'fast' }, { label: 'Think', value: 'think' }, { label: 'Super', value: 'super' } ]; function Shape() { const [round, setRound] = useState(false); const renderLeftMenu = useCallback(() => <> } field="onlineSearch">Web search ); const onChange = useCallback((e) => { setRound(e.target.value); }, []); return (<> Rounded Square ); }; render(); ``` ### Suggestions Configure suggestions with the `suggestion` 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`. ```jsx live=true dir="column" noInline=true import React from 'react'; import { AIChatInput } from '@douyinfe/semi-ui'; const uploadProps = { action: "https://api.semi.design/upload" }; const outerStyle = { margin: 12 }; const suggestionTemplate = [ 'Weather', 'Air Quality', 'Work Progress', 'Schedule' ]; function Suggestion() { const [suggestion, setSuggestion] = useState([]); const onChange = useCallback((content) => { let value; if (content.length && content[0].text) { value = content[0].text; } if (value === undefined) { if (suggestion === undefined || suggestion.length === 0) { return; } else { return setSuggestion([]); } } if (value.length === 0) { setSuggestion([]); } else if (value.length > 0 && value.length < 4) { const su = new Array(suggestionTemplate.length).fill(0).map((item, index) => `${value}, ${suggestionTemplate[index]}` ); setSuggestion(su); } else if (value.length >= 4) { setSuggestion([]); } }, [suggestion]); return ( ); } render(); ``` ### Skills & Templates Configure a skill list with `skills`, and use `skillHotKey` to set the shortcut for skill panel. - `skills` sample format: ```ts 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. ```jsx live=true dir="column" noInline=true import React from 'react'; import { AIChatInput } from '@douyinfe/semi-ui'; import { IconTemplateStroked, IconSearch } from '@douyinfe/semi-icons'; 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: , value: 'writing', label: 'Writing', hasTemplate: true, }, { icon: , value: 'AI coding', label: 'AI coding' }, ]; const template = [ { groupKey: 'value', group: 'Work', children: [ { bg: 'var(--semi-color-primary)', icon: , title: 'Summary report', desc: 'Condensate your work results', content: `My occupation is . Please help me write a summary report on , please help me write a paragraph for unfamiliar colleagues` } ] }, { groupKey: 'marketing', group: 'Marketing', children: [ { bg: 'var(--semi-color-primary)', icon: , title: 'Promotional copy', desc: 'Write promotional copy for each platform', content: 'Please help me write a promotional copy for professionals about . It needs to directly hit the pain points and attract users to click.' }, { bg: 'var(--semi-color-warning)', icon: , title: 'Program planning', desc: 'Tailor-made solutions', content: 'I am a professional planner. Please help me write a offline book club activity plan, which should include but not be limited to planning goals, detailed plans, required resources and budget, effect evaluation, risk response, etc.' } ] } ]; const TemplateContent = (props) => { const { onTemplateClick: onTemplateClickProps } = props; const [groupIndex, setGroupIndex] = useState(0); const onItemClick = useCallback((e) => { const index = e.target.dataset.index; setGroupIndex(Number(index)); }, []); const onTemplateClick = useCallback((item) => { const { content } = item; onTemplateClickProps(content); }, [onTemplateClickProps]); return (
{/* tabs */}
{(template ? template : []).map((item, index) => { return (
{item.group}
); })}
{/* content */}
{(((template ? template : [])[groupIndex] ? (template ? template : [])[groupIndex] : {}).children ? (template ? template : [])[groupIndex].children : []).map((item, index) => (
onTemplateClick(item)} >
{item.icon}
{item.title}
{item.desc}
))}
); }; function Template() { const ref = useRef(); const setTemplate = useCallback((content) => { const element = ref.current; if (!element) { return; } element.setContentWhileSaveTool(content); element.focusEditor(); }, [ref]); const renderTemplate = useCallback((skill = {}, e) => { if (skill.value === 'writing') { return ; } }, [setTemplate]); const renderLeftMenu = useCallback(() => <> ); return ( ); }; render(