AI Component · AIChatInput

AIChatInput

Input box used in AI chat scenarios

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

<script setup lang="ts">
    import { AIChatInput, getConfigureItem } from '@fibrous/semi-ai-vue';
</script>

Basic Usage

Supports text input and file upload. You can configure the following parameters as needed:

  • uploadProps: Configure parameters related to file upload. It takes the props of Upload except fileList and listType; pass Upload's events as listener props such as onSuccess and onError
  • @uploadChange: Emitted when file upload changes
  • When deleting uploaded files, uploadProps.onRemove will be called, and uploadProps.beforeRemove will be respected (Promise supported)
  • placeholder: Placeholder for the input box
  • defaultContent: Default content for the input box
  • @contentChange: Emitted when the content of the input box changes; the argument is the current content
Code
<script setup lang="ts">
    import { AIChatInput, type AIChatInputContent as Content } from '@fibrous/semi-ai-vue';
    import type { UploadOnChangeProps as OnChangeProps } from '@fibrous/semi-ui-vue';

    const uploadProps = { action: 'https://api.semi.design/upload' };

    const onContentChange = (content: Content[]) => {
        console.log('onContentChange', content);
    };

    const onUploadChange = (fileList: OnChangeProps) => {
        console.log('onUploadChange', fileList);
    };
</script>

<template>
    <AIChatInput placeholder="输入内容或者上传内容..." style="margin: 12px" :uploadProps="uploadProps" @contentChange="onContentChange" @uploadChange="onUploadChange" />
</template>

Message Sending

When there is content in the input box (text entry or uploaded content), sending messages is allowed. Clicking the send message button emits @messageSend; 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 emits @stopGenerate, where you can handle logic such as setting generating to false.

dy.jpeg
jpeg 130kb
resso.jpeg
jpeg 222kb
Code
<script setup lang="ts">
    import { ref } from 'vue';
    import { AIChatInput, type AIChatInputContent as Content, type AIChatInputReference as Reference, type AIChatInputUploadProps } from '@fibrous/semi-ai-vue';
    import type { UploadOnChangeProps } from '@fibrous/semi-ui-vue';

    const uploadProps: AIChatInputUploadProps = {
        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 references = ref<Reference[]>([
        {
            id: '1',
            type: 'text',
            content: '测试文本,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字,这里是一段很长的文字',
        },
    ]);
    const generating = ref(false);

    const onContentChange = (content: Content[]) => {
        console.log('onContentChange', content);
    };

    const onUploadChange = (fileList: UploadOnChangeProps) => {
        console.log('onUploadChange', fileList);
    };

    const toggleGenerate = () => {
        generating.value = !generating.value;
    };

    const onMessageSend = () => {
        toggleGenerate();
        references.value = [];
    };

    const handleReferenceDelete = (item: Reference) => {
        references.value = references.value.filter((ref) => ref.id !== item.id);
    };
</script>

<template>
    <AIChatInput
        defaultContent="点击发送按钮,观察上传内容、引用内容、输入框内容变化"
        :generating="generating"
        :references="references"
        style="margin: 12px"
        :uploadProps="uploadProps"
        @contentChange="onContentChange"
        @messageSend="onMessageSend"
        @referenceDelete="handleReferenceDelete"
        @stopGenerate="toggleGenerate"
        @uploadChange="onUploadChange"
    />
</template>

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 method of the component instance (template ref) and focus the editor with focusEditor.

Code
<script setup lang="ts">
    import { ref, useTemplateRef } from 'vue';
    import { AIChatInput, type AIChatInputHandle } from '@fibrous/semi-ai-vue';

    const uploadProps = { action: 'https://api.semi.design/upload' };
    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> 帮我完成...`,
    };
    const names = Object.keys(temp);
    const contents = Object.values(temp);

    const activeIndex = ref(0);
    const input = useTemplateRef<AIChatInputHandle>('input');

    const setTemplate = (index: number) => {
        activeIndex.value = index;
        input.value?.setContent(contents[index]);
        input.value?.focusEditor();
    };
</script>

<template>
    <div class="aiChatInput-radio" role="tablist">
        <div v-for="(item, index) in names" :key="item" :aria-selected="index === activeIndex" :class="['aiChatInput-radio-item', { 'aiChatInput-radio-item-selected': index === activeIndex }]" role="tab" tabindex="0" @click="setTemplate(index)" @keydown.enter="setTemplate(index)">
            {{ item }}
        </div>
    </div>
    <AIChatInput ref="input" :defaultContent="temp['input-slot']" placeholder="输入内容或者上传内容" style="margin: 12px" :uploadProps="uploadProps" />
</template>

Reference

You can pass in references via the references, which will display at the top of the input box.

  • #reference slot: Custom renderer for an individual reference, with the scope parameters { item, index }.
  • @referenceDelete: Emitted when a reference is deleted.
  • @referenceClick: Emitted when a reference is clicked.
Code
<script setup lang="ts">
    import { ref } from 'vue';
    import { AIChatInput, type AIChatInputReference as Reference } from '@fibrous/semi-ai-vue';

    const uploadProps = { action: 'https://api.semi.design/upload' };
    const references = ref<Reference[]>([
        {
            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',
        },
    ]);

    const handleReferenceDelete = (item: Reference) => {
        references.value = references.value.filter((ref) => ref.id !== item.id);
    };

    const handleReferenceClick = (item: Reference) => {
        console.log('点击了引用', item);
    };
</script>

<template>
    <AIChatInput placeholder="用于查看引用内容的用例" :references="references" style="margin: 12px" :uploadProps="uploadProps" @referenceClick="handleReferenceClick" @referenceDelete="handleReferenceDelete" />
</template>

Configuration Area

You can configure options such as model parameters, web search, and critical thinking through the configuration area, or display/view MCP tools.

  • #configureArea slot: Custom content for config area buttons.
  • Use AIChatInput.Configure components such as Select, Button, Mcp, RadioButton, etc.

AIChatInput manages the state and emits @configureChange (make sure to set the unique field). For initial values, use initValue. The value change of a single item is also available through its @change, with the new value as the argument.

You can also use getConfigureItem to adapt your custom components.

Code
<script setup lang="ts">
    import type { Component } from 'vue';
    import { AIChatInput, type AIChatInputLeftMenuChangeProps as LeftMenuChangeProps } from '@fibrous/semi-ai-vue';
    import { IconBookOpenStroked, IconFeishuLogo, IconGit, IconFigma } from '@fibrous/semi-icons-vue';

    const { Configure } = AIChatInput;
    const uploadProps = { action: 'https://api.semi.design/upload' };

    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 = [
        { label: '飞书文档', value: 'feishu' },
        { label: 'Github Mcp', value: 'github' },
        { label: 'IconFigma Mcp', value: 'IconFigma' },
    ];
    const mcpIcons: Record<string, Component> = { feishu: IconFeishuLogo, github: IconGit, IconFigma };

    const radioButtonProps = [
        { label: '极速', value: 'fast' },
        { label: '思考', value: 'think' },
        { label: '超能', value: 'super' },
    ];

    const onConfigureButtonClick = () => {
        console.log('onConfigureButtonClick');
    };

    const onConfigureChange = (value: LeftMenuChangeProps, changedValue?: LeftMenuChangeProps) => {
        console.log('onConfigureChange', value, changedValue);
    };
</script>

<template>
    <AIChatInput placeholder="用于查看左下方配置项的用例" style="margin: 12px" :uploadProps="uploadProps" @configureChange="onConfigureChange">
        <template #configureArea>
            <Configure.Select field="model" initValue="GPT-4o" :optionList="modelOptions" />
            <Configure.Button field="onlineSearch">
                <template #icon>
                    <IconBookOpenStroked />
                </template>
                联网搜索
            </Configure.Button>
            <Configure.Mcp :options="mcpOptions" showConfigure @configureButtonClick="onConfigureButtonClick">
                <template #optionIcon="{ item }">
                    <component :is="mcpIcons[item.value]" />
                </template>
            </Configure.Mcp>
            <Configure.RadioButton field="thinkType" initValue="fast" :options="radioButtonProps" />
        </template>
    </AIChatInput>
</template>

You can extend any custom component for configuration using getConfigureItem.

function getConfigureItem<C extends Component, VK extends keyof PropsOf<C>, CK extends keyof PropsOf<C>>(
    component: C,
    opts?: {
        // the prop that receives the value, modelValue by default
        valueKey?: VK;
        // the listener called when the value changes, onUpdate:modelValue by default
        onKeyChangeFnName?: CK;
        className?: string;
        defaultProps?: Partial<PropsOf<C>>;
    }
): Component<
    Omit<PropsOf<C>, VK | CK | 'onChange'> & {
        // the key of this item in the AIChatInput configure value, required
        field: string;
        initValue?: PropsOf<C>[VK];
    }
>;

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. The other props, listeners and slots of the wrapped component are passed on as they are. The first argument of the value listener must be the new value, which the item also emits through its @change event.

Demo:

Code
<script setup lang="ts">
    import { AIChatInput, getConfigureItem, type AIChatInputLeftMenuChangeProps as LeftMenuChangeProps } from '@fibrous/semi-ai-vue';
    import { Cascader } from '@fibrous/semi-ui-vue';

    const uploadProps = { action: 'https://api.semi.design/upload' };
    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 CustomCascader = getConfigureItem(Cascader, {
        className: 'aiChatInput-cascader-configure',
    });

    const onConfigureChange = (value: LeftMenuChangeProps, changedValue?: LeftMenuChangeProps) => {
        console.log('onConfigureChange', value, changedValue);
    };
</script>

<template>
    <AIChatInput placeholder="用于查看左下方配置项的用例" style="margin: 12px" :uploadProps="uploadProps" @configureChange="onConfigureChange">
        <template #configureArea>
            <CustomCascader field="model" :initValue="['GPT', 'GPT-4o']" :treeData="cascaderModalOptions" />
        </template>
    </AIChatInput>
</template>

Action Area

The lower right corner is the action area. Use the #actionArea slot 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; render them with <component :is>.

interface AIChatInputActionAreaSlotProps {
    menuItem: VNode[];
    class: string;
}
Code
<script setup lang="ts">
    import { AIChatInput } from '@fibrous/semi-ai-vue';
    import { IconDeleteStroked } from '@fibrous/semi-icons-vue';
    import { Divider, Button } from '@fibrous/semi-ui-vue';

    const uploadProps = { action: 'https://api.semi.design/upload' };
</script>

<template>
    <AIChatInput placeholder="输入内容或者上传内容..." style="margin: 12px" :uploadProps="uploadProps">
        <template #actionArea="{ menuItem, class: actionClass }">
            <div :class="actionClass">
                <div style="display: flex; align-items: center">
                    <Button aria-label="清空内容" style="border-radius: 50%" type="tertiary">
                        <template #icon>
                            <IconDeleteStroked />
                        </template>
                    </Button>
                    <Divider layout="vertical" style="margin-left: 8px" />
                </div>
                <component :is="node" v-for="(node, index) in menuItem" :key="index" />
            </div>
        </template>
    </AIChatInput>
</template>

Custom Upload Button

By default, an upload button is rendered on the left side of the footer action area. Use the #uploadButton slot 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). onOpenFileDialog triggers the internal Upload file chooser.

interface AIChatInputUploadButtonSlotProps {
    defaultNode: VNode;
    onOpenFileDialog: () => void;
    disabled: boolean;
    attachments: Attachment[];
}
Code
<script setup lang="ts">
    import { AIChatInput } from '@fibrous/semi-ai-vue';
    import { IconUpload } from '@fibrous/semi-icons-vue';

    const uploadProps = { action: 'https://api.semi.design/upload' };
</script>

<template>
    <AIChatInput placeholder="自定义上传按钮(仍支持粘贴上传)" style="margin: 12px" :uploadProps="uploadProps">
        <template #uploadButton="{ onOpenFileDialog, disabled }">
            <button aria-label="上传" class="semi-button semi-button-borderless" :disabled="disabled" type="button" @click.stop="onOpenFileDialog">
                <IconUpload />
            </button>
        </template>
    </AIChatInput>
</template>

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
<script setup lang="ts">
    import { ref } from 'vue';
    import { AIChatInput } from '@fibrous/semi-ai-vue';
    import { IconBookOpenStroked, IconFeishuLogo, IconGit, IconFigma } from '@fibrous/semi-icons-vue';
    import { RadioGroup, Radio, type RadioValue } from '@fibrous/semi-ui-vue';

    const { Configure } = AIChatInput;
    const uploadProps = { action: 'https://api.semi.design/upload' };
    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 = [
        { label: '飞书文档', value: 'feishu' },
        { label: 'Github Mcp', value: 'github' },
        { label: 'IconFigma Mcp', value: 'IconFigma' },
    ];
    const mcpIcons = { feishu: IconFeishuLogo, github: IconGit, IconFigma };

    const radioButtonProps = [
        { label: '极速', value: 'fast' },
        { label: '思考', value: 'think' },
        { label: '超能', value: 'super' },
    ];

    const shape = ref<RadioValue | undefined>('square');
</script>

<template>
    <RadioGroup v-model="shape" aria-label="单选组合示例" name="demo-radio-group">
        <Radio value="round">圆形</Radio>
        <Radio value="square">方形</Radio>
    </RadioGroup>
    <AIChatInput placeholder="下方按钮为方形的用例" :round="shape === 'round'" style="margin: 12px" :uploadProps="uploadProps">
        <template #configureArea>
            <Configure.Select field="model" initValue="GPT-4o" :optionList="modelOptions" />
            <Configure.Button field="onlineSearch">
                <template #icon>
                    <IconBookOpenStroked />
                </template>
                联网搜索
            </Configure.Button>
            <Configure.Mcp :options="mcpOptions">
                <template #optionIcon="{ item }">
                    <component :is="mcpIcons[item.value as keyof typeof mcpIcons]" />
                </template>
            </Configure.Mcp>
            <Configure.RadioButton field="mode" initValue="fast" :options="radioButtonProps" />
        </template>
    </AIChatInput>
</template>

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 the #suggestionItem slot; bind the scope parameters other than suggestion, that is id, class, role, aria-selected, onClick and onMouseenter, onto the root element with v-bind.

Code
<script setup lang="ts">
    import { ref } from 'vue';
    import { AIChatInput, type AIChatInputContent as Content } from '@fibrous/semi-ai-vue';

    const uploadProps = { action: 'https://api.semi.design/upload' };
    const suggestionTemplate = ['天气如何', '空气质量', '工作进程', '日程安排'];

    const suggestions = ref<string[]>([]);

    const onChange = (content: Content[]) => {
        const value: string | undefined = content.length && content[0].text ? content[0].text : undefined;
        if (value === undefined || value.includes('\n') || value.length >= 4) {
            if (suggestions.value.length) {
                suggestions.value = [];
            }
            return;
        }
        suggestions.value = value.length ? suggestionTemplate.map((item) => `${value},${item}`) : [];
    };
</script>

<template>
    <AIChatInput placeholder="输入内容,当内容长度小于 4个字符可以看到建议,使用上下按键可切换侯选项" style="margin: 12px" :suggestions="suggestions" :uploadProps="uploadProps" @contentChange="onChange" />
</template>

Skills & Templates

Configure a skill list with skills, and use skillHotKey to set the shortcut for skill panel.

  • skills sample format:
interface Skill {
    label?: string;
    value?: string;
    // If this skill has a template, set hasTemplate to true, affects the display of template display buttons
    hasTemplate?: boolean;
}

A skill holds data only. The icon before a skill goes through the #skillIcon="{ item, index }" slot, and the whole item can be customized with the #skillItem slot (bound like a suggestion item).

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 #template slot. The template panel can be displayed and closed by clicking the template button.

interface AIChatInputTemplateSlotProps {
    skill: Skill | undefined;
    // called when a template is clicked, which replaces the input content with the template content
    onTemplateClick: (content: string) => void;
}

Usage examples are as follows:

Code
<script setup lang="ts">
    import { ref, useTemplateRef } from 'vue';
    import { AIChatInput, type AIChatInputHandle } from '@fibrous/semi-ai-vue';
    import { IconTemplateStroked, IconSearch } from '@fibrous/semi-icons-vue';

    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 skills = [
        {
            value: 'writing',
            label: '帮我写作',
            hasTemplate: true,
        },
        {
            value: 'AI 编程',
            label: 'AI coding',
        },
    ];
    const skillIcons: Record<string, typeof IconSearch> = { writing: IconTemplateStroked, 'AI 编程': IconSearch };

    const templates = [
        {
            group: '工作',
            children: [
                {
                    bg: 'var(--semi-color-primary)',
                    title: '总结汇报',
                    desc: '凝练你的工作成效',
                    content: `我的职业是<input-slot placeholder="[请输入职业]"></input-slot>,帮我写一份关于<input-slot placeholder="[输入目的:项目进展总结、团队工作成果或其他]"></input-slot>的总结汇报`,
                },
                {
                    bg: 'var(--semi-color-warning)',
                    title: '话术',
                    desc: '满足不同场景表达需求',
                    content: `我是一名<select-slot value="打工人" options='["打工人","学生"]'></select-slot> ,帮我写一段面向<input-slot placeholder="[输入对象]">陌生同事</input-slot>的话术内容`,
                },
            ],
        },
        {
            group: '商业营销',
            children: [
                {
                    bg: 'var(--semi-color-primary)',
                    title: '宣传文案',
                    desc: '撰写各平台的推广文案',
                    content: '帮我写一篇面向<input-slot placeholder="[输入目标人群]"></input-slot>职场人士,关于<input-slot placeholder="[输入产品]"></input-slot>的宣传文案,需要直击痛点,吸引用户点击。',
                },
                {
                    bg: 'var(--semi-color-warning)',
                    title: '方案策划',
                    desc: '量身定制各种方案',
                    content: '我是一名<input-slot placeholder="[输入职业]"></input-slot>职业策划人 ,帮我写一个<input-slot placeholder="[方案类型:如线下读书会活动方案等]"></input-slot>线下读书会活动 的方案,需要包含但不限于策划目标、详细计划、所需资源和预算、效果评估、风险应对等。',
                },
            ],
        },
    ];

    const groupIndex = ref(0);
    const input = useTemplateRef<AIChatInputHandle>('input');

    const setTemplate = (content: string) => {
        input.value?.setContentWhileSaveTool(content);
        input.value?.focusEditor();
    };
</script>

<template>
    <AIChatInput ref="input" placeholder="输入 / 唤起技能,选择技能后,点击模版按钮可查看模版,可通过鼠标上下按键切换侯选项" skillHotKey="/" :skills="skills" style="margin: 12px" :uploadProps="uploadProps">
        <template #configureArea>
            <Configure.Select field="model" initValue="GPT-4o" :optionList="modelOptions" />
        </template>
        <template #skillIcon="{ item }">
            <component :is="skillIcons[item.value ?? '']" />
        </template>
        <template #template="{ skill }">
            <div v-if="skill?.value === 'writing'" class="aiChatInput-template">
                <div class="template-header">
                    <button v-for="(item, index) in templates" :key="item.group" :class="['template-header-item', { 'template-header-item-active': groupIndex === index }]" style="border: none; font-family: inherit" type="button" @click="groupIndex = index">
                        {{ item.group }}
                    </button>
                </div>
                <div class="template-content">
                    <button v-for="item in templates[groupIndex].children" :key="item.title" class="template-content-item" style="font: inherit; text-align: left" type="button" @click="setTemplate(item.content)">
                        <span class="template-content-item-icon" :style="{ background: item.bg }">
                            <IconTemplateStroked />
                        </span>
                        <span class="template-content-item-title">{{ item.title }}</span>
                        <span class="template-content-item-desc">{{ item.desc }}</span>
                    </button>
                </div>
            </div>
        </template>
    </AIChatInput>
</template>

Custom Top Slot

Users can customize the top rendering area using the #topSlot slot, 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 AIChatInputTopSlotProps {
    references: Reference[];
    attachments: Attachment[];
    // User input into the input box
    content: Content[];
    onUploadFileDelete: (attachment: Attachment) => void;
    // Emits @referenceDelete; the caller owns the references and removes the item in the event
    onReferenceDelete: (reference: Reference) => void;
}

Usage examples are as follows:

horizontalScroller.tsx
AIChatInput
web
recentChange
Branch Diff with Main Branch
From 1-2
Code
<script setup lang="ts">
    import { ref, type Component } from 'vue';
    import { AIChatInput, type AIChatInputAttachment as Attachment, type AIChatInputReference as Reference } from '@fibrous/semi-ai-vue';
    import { IconClose, IconUpload, IconFile, IconFolder, IconBranch, IconTerminal, IconGlobeStroke, IconConnectionPoint2, IconTemplateStroked, IconSearch, IconGit, IconCode } from '@fibrous/semi-icons-vue';

    const { Configure } = AIChatInput;
    const radioButtonProps = [
        { label: '快速', value: 'fast' },
        { label: '深度思考', value: 'think' },
    ];
    const modeIcons: Record<string, Component> = { fast: IconTemplateStroked, think: IconSearch };
    const uploadProps = { action: 'https://api.semi.design/upload' };

    const references = ref<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: Record<string, Component> = {
        file: IconFile,
        folder: IconFolder,
        branch: IconBranch,
        terminal: IconTerminal,
        web: IconGlobeStroke,
        change: IconConnectionPoint2,
        git: IconGit,
        code: IconCode,
    };

    const onReferenceDelete = (reference: Reference) => {
        references.value = references.value.filter((item) => item.id !== reference.id);
    };
</script>

<template>
    <AIChatInput class="aiChatInput-customTopSlot" placeholder="自定义渲染顶部内容,可用于渲染上传内容、引用内容" :references="references" :showReference="false" :showUploadFile="false" style="margin: 12px" :uploadProps="uploadProps" @referenceDelete="onReferenceDelete">
        <template #topSlot="{ attachments, references: items, onReferenceDelete: deleteReference, onUploadFileDelete }">
            <div class="ai-chat-input-topSlot">
                <div v-for="item in items" :key="item.id" class="item">
                    <span class="item-icon">
                        <component :is="refTypeToIconMap[item.type ?? '']" v-if="item.type && refTypeToIconMap[item.type]" class="item-left item-icon" size="small" />
                        <button aria-label="删除" class="item-icon-delete" type="button" @click="deleteReference(item)">
                            <IconClose size="small" />
                        </button>
                    </span>
                    <span class="item-content">
                        {{ item.name }}
                        <span v-if="item.type === 'branch'" class="detail">{{ item.detail }}</span>
                    </span>
                </div>
                <div v-for="item in attachments" :key="item.uid" class="item">
                    <span class="item-icon">
                        <img v-if="isImageType(item)" :alt="item.name" class="item-image item-left" :src="item.url" />
                        <IconUpload v-else class="item-left item-icon" size="small" />
                        <button aria-label="删除" class="item-icon-delete" type="button" @click="onUploadFileDelete(item)">
                            <IconClose size="small" />
                        </button>
                    </span>
                    <span class="item-content">{{ item.name }}</span>
                </div>
            </div>
        </template>
        <template #configureArea>
            <Configure.RadioButton field="mode" initValue="fast" :options="radioButtonProps">
                <!-- the icon is the only label, so it carries the name -->
                <template #label="{ option }">
                    <component :is="modeIcons[String(option.value)]" :aria-label="option.label" />
                </template>
            </Configure.RadioButton>
        </template>
    </AIChatInput>
</template>

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.

Render the view of a custom node with VueNodeViewRenderer of @tiptap/vue-3. If you add a custom extension, you must configure the corresponding transformation rules in transformer to ensure that the data emitted by @contentChange matches your expectations.

When adding a custom extension, please note the following:

  • Please add the isCustomSlot property to your custom extension. This property is related to the cursor height before and after the custom extension.
  • Since AIChatInput uses Enter as the send hotkey, if your custom extension uses Enter as a shortcut, you need to manually configure SemiAIChatInput.allowHotKeySend in editor.storage to indicate whether the hotkey should be used by AIChatInput for sending, in order to avoid hotkey conflicts. When sendHotKey is shift+enter, a plain Enter no longer breaks the line while allowHotKeySend is false; it is left to the custom extension.

An example of a custom extension definition and related notes is as follows:

horizontalScroller.tsx
AIChatInput
web
recentChange
Branch Diff with Main Branch
From 1-2
Code
<script setup lang="ts">
    import { computed, defineComponent, h, ref, shallowRef, useTemplateRef, watch, type Component, type CSSProperties } from 'vue';
    import { computePosition, shift } from '@floating-ui/dom';
    import { Node, mergeAttributes } from '@tiptap/core';
    import { Mention, type MentionNodeAttrs, type MentionOptions } from '@tiptap/extension-mention';
    import { NodeViewWrapper, VueNodeViewRenderer, nodeViewProps, posToDOMRect } from '@tiptap/vue-3';
    import { AIChatInput, type AIChatInputHandle, type AIChatInputAttachment as Attachment, type AIChatInputContent as Content, type AIChatInputReference as Reference } from '@fibrous/semi-ai-vue';
    import { IconFile, IconFolder, IconBranch, IconCode, IconGit, IconGlobeStroke, IconChevronRight, IconClose, IconUpload, IconTerminal, IconConnectionPoint2 } from '@fibrous/semi-icons-vue';
    import { useMounted } from '@fibrous/semi-ui-vue/hooks';

    const uploadProps = { action: 'https://api.semi.design/upload' };

    interface PanelItem {
        icon: Component;
        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);

    // Node views must be components but a demo file holds one SFC, so it is a render function
    const ReferSlotView = defineComponent({
        props: nodeViewProps,
        setup: (props) => () => h(NodeViewWrapper, { as: 'span', class: 'ai-chat-input-refer-slot-wrapper' }, () => h('span', { class: 'ai-chat-input-refer-slot' }, props.node.attrs.value ?? '')),
    });

    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 VueNodeViewRenderer(ReferSlotView);
        },
    });

    interface ReferSlotAttrs {
        value: string;
        info: string;
        type?: string;
        uniqueKey: string;
    }

    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;
    }

    type MentionPanelProps = Omit<MentionStartProps, 'command'> & {
        command: (props: MentionCommandProps) => void;
    };

    // The Teleport target is the browser's body, which the server render and the hydration pass do not have
    const mounted = useMounted();
    // Suggestion panel state; null means closed
    const panel = shallowRef<MentionPanelProps | null>(null);
    const level = ref(1);
    const options = shallowRef<(string | PanelItem)[]>(FirstLevel);
    const selectedIndex = ref(0);
    const floating = useTemplateRef<HTMLElement>('floating');
    const floatingStyle = ref<CSSProperties>({ position: 'absolute', width: 'max-content', left: '0px', top: '0px' });

    const filterOptions = computed(() => {
        const query = panel.value?.query;
        return query
            ? options.value.filter((item) => {
                  const name = typeof item === 'string' ? item : item.name;
                  return name.toLowerCase().includes(query.toLowerCase());
              })
            : options.value;
    });

    watch([options, () => panel.value?.query], () => {
        selectedIndex.value = 0;
    });

    // The panel follows the caret; option changes resize it, so reposition
    watch(
        [panel, filterOptions],
        () => {
            const current = panel.value;
            if (!current?.clientRect || !floating.value) return;
            const virtualElement = {
                getBoundingClientRect: () => posToDOMRect(current.editor.view, current.editor.state.selection.from, current.editor.state.selection.to),
            };
            void computePosition(virtualElement, floating.value, {
                placement: 'bottom-start',
                strategy: 'absolute',
                middleware: [shift()],
            }).then(({ x, y, strategy }) => {
                floatingStyle.value = { position: strategy, width: 'max-content', left: `${x}px`, top: `${y}px` };
            });
        },
        { flush: 'post' }
    );

    const setHotKeySend = (allow: boolean) => {
        // While the panel is open, Enter belongs to it, not to AIChatInput's send
        panel.value?.command({ allowHotKeySend: allow });
    };

    const closePanel = () => {
        setHotKeySend(true);
        panel.value = null;
    };

    const selectItem = (item: string | PanelItem) => {
        if (typeof item === 'string') {
            level.value = 2;
            options.value = TestAction[item];
        } else {
            panel.value?.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.value === 1) {
            panel.value?.editor.commands.focus();
        }
    };

    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: () => ({
            onStart: (props) => {
                level.value = 1;
                options.value = FirstLevel;
                panel.value = props as MentionPanelProps;
                setHotKeySend(false);
            },
            onUpdate: (props) => {
                // After Escape closes the panel it stays closed until this suggestion ends
                if (panel.value) {
                    panel.value = props as MentionPanelProps;
                }
            },
            onKeyDown: ({ event }) => {
                if (!panel.value) return false;
                if (event.key === 'ArrowUp') {
                    selectedIndex.value = (selectedIndex.value + filterOptions.value.length - 1) % filterOptions.value.length;
                    return true;
                }
                if (event.key === 'ArrowDown') {
                    selectedIndex.value = (selectedIndex.value + 1) % filterOptions.value.length;
                    return true;
                }
                if (event.key === 'Enter') {
                    const item = filterOptions.value[selectedIndex.value];
                    if (item) {
                        selectItem(item);
                    }
                    return true;
                }
                if (event.key === 'Escape') {
                    if (level.value === 1) {
                        closePanel();
                    } else {
                        level.value = 1;
                        options.value = FirstLevel;
                    }
                    return true;
                }
                return false;
            },
            onExit: closePanel,
        }),
    };

    const extensions = [
        ReferSlot,
        Mention.configure({
            HTMLAttributes: {
                class: 'mention',
            },
            suggestion,
        }),
    ];

    const references = ref<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: Record<string, Component> = {
        file: IconFile,
        folder: IconFolder,
        branch: IconBranch,
        terminal: IconTerminal,
        web: IconGlobeStroke,
        change: IconConnectionPoint2,
        git: IconGit,
        code: IconCode,
    };

    const input = useTemplateRef<AIChatInputHandle>('input');

    const richContent = (content: Content[]) => content.filter((item) => item.type !== 'text');

    const onReferenceDelete = (reference: Reference) => {
        references.value = references.value.filter((item) => item.id !== reference.id);
    };

    const onContentChange = (content: Content[]) => {
        console.log('onContentChange', content);
    };

    const transformer = new Map([
        [
            'referSlot',
            ({ attrs }: { attrs: ReferSlotAttrs }) => {
                const { value, info, type = 'text', uniqueKey } = attrs;
                return {
                    type: type,
                    value: value,
                    uniqueKey: uniqueKey,
                    ...JSON.parse(info),
                };
            },
        ],
    ]);
</script>

<template>
    <AIChatInput
        ref="input"
        class="aiChatInput-customTopSlot"
        :extensions="extensions"
        placeholder="使用 @ 触发"
        :references="references"
        :showReference="false"
        :showUploadFile="false"
        style="margin: 12px"
        :transformer="transformer"
        :uploadProps="uploadProps"
        @contentChange="onContentChange"
        @referenceDelete="onReferenceDelete"
    >
        <template #topSlot="{ attachments, content, references: items, onReferenceDelete: deleteReference, onUploadFileDelete }">
            <div class="ai-chat-input-topSlot">
                <div v-for="(item, index) in richContent(content)" :key="item.key ?? index" class="item">
                    <span class="item-icon">
                        <component :is="refTypeToIconMap[item.type]" v-if="refTypeToIconMap[item.type]" class="item-left item-icon" size="small" />
                        <button aria-label="删除" class="item-icon-delete" type="button" @click="input?.deleteContent(item)">
                            <IconClose size="small" />
                        </button>
                    </span>
                    <span class="item-content">
                        {{ item.name ? item.name : item.value }}
                        <span v-if="item.type === 'branch'" class="detail">{{ item.detail }}</span>
                    </span>
                </div>
                <div v-for="item in items" :key="item.id" class="item">
                    <span class="item-icon">
                        <component :is="refTypeToIconMap[item.type ?? '']" v-if="item.type && refTypeToIconMap[item.type]" class="item-left item-icon" size="small" />
                        <button aria-label="删除" class="item-icon-delete" type="button" @click="deleteReference(item)">
                            <IconClose size="small" />
                        </button>
                    </span>
                    <span class="item-content">
                        {{ item.name }}
                        <span v-if="item.type === 'branch'" class="detail">{{ item.detail }}</span>
                    </span>
                </div>
                <div v-for="item in attachments" :key="item.uid" class="item">
                    <span class="item-icon">
                        <img v-if="isImageType(item)" :alt="item.name" class="item-image item-left" :src="item.url" />
                        <IconUpload v-else class="item-left item-icon" size="small" />
                        <button aria-label="删除" class="item-icon-delete" type="button" @click="onUploadFileDelete(item)">
                            <IconClose size="small" />
                        </button>
                    </span>
                    <span class="item-content">{{ item.name }}</span>
                </div>
            </div>
        </template>
    </AIChatInput>
    <Teleport v-if="mounted" to="body">
        <div v-if="panel" ref="floating" :style="floatingStyle">
            <div class="ai-chat-input-custom-extension-dropdown-menu" role="menu" :style="{ width: level === 1 ? '200px' : '300px' }">
                <template v-if="filterOptions.length">
                    <div v-for="(item, index) in filterOptions" :key="index" :class="['optionItem', { 'is-selected': index === selectedIndex }]" role="menuitem" tabindex="-1" @click="onItemClick(item)" @focusin="selectedIndex = index" @keydown.enter="onItemClick(item)" @mouseenter="selectedIndex = index">
                        <span v-if="typeof item === 'string'">{{ item }}</span>
                        <div v-else class="level2Item">
                            <component :is="item.icon" />
                            <span class="name">{{ item.name }}</span>
                            <span class="path">{{ item.path }}</span>
                        </div>
                        <IconChevronRight v-if="level === 1" class="option-item-arrow" />
                    </div>
                </template>
                <div v-else class="item">No result</div>
            </div>
        </div>
    </Teleport>
</template>

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

PropertyDescriptionTypeDefault
canSendWhether sending is allowed. When unset, it depends on the input content and the uploaded files; references do not count.booleansemiGlobalDefault("AIChatInput", "canSend")
clearContentOnGeneratingWhether the input content and attachments are cleared when generating turns on. Set it to false to keep the input (for non-conversational scenes such as an infinite canvas).booleansemiDefault("AIChatInput", "clearContentOnGenerating", true)
defaultContentDefault content of the input, an html string or JSON like Tiptap's Content. Read once, when the editor is created.ContentsemiGlobalDefault("AIChatInput", "defaultContent")
dropdownMatchTriggerWidthWhether the dropdown layer has the same width as the input.booleansemiDefault("AIChatInput", "dropdownMatchTriggerWidth", true)
extensionsCustom tiptap extensions. Read once, when the editor is created; later changes have no effect.ExtensionssemiGlobalDefault("AIChatInput", "extensions")
generatingWhether a response is being generated. While true, the send button becomes the stop button.booleansemiDefault("AIChatInput", "generating", false)
keepSkillAfterSendWhether the selected skill is kept after a message is sent.booleansemiDefault("AIChatInput", "keepSkillAfterSend", false)
placeholderPlaceholder of the input: a string, or a function that returns a string for a node.PlaceholderPropsundefined
popoverPropsProps of the dropdown layer. The class is merged with the internal class names and the style with the panel width.AIChatInputPopoverPropssemiGlobalDefault("AIChatInput", "popoverProps")
referencesReferences shown with the input.Reference[]semiGlobalDefault("AIChatInput", "references")
roundWhether the buttons of the footer configure and action areas are fully rounded.booleansemiDefault("AIChatInput", "round", true)
sendHotKeyKeyboard shortcut that sends the input. enter sends on Enter alone and breaks the line on Shift+Enter; shift+enter is the reverse."enter" | "shift+enter"semiDefault("AIChatInput", "sendHotKey", "enter")
showPlaceholderWhenSkillOnlyWhether the placeholder shows when only a skill is selected, after the skill. Read once, when the editor is created.booleansemiDefault("AIChatInput", "showPlaceholderWhenSkillOnly", false)
showReferenceWhether the reference area is shown; use it together with #topSlot.booleansemiDefault("AIChatInput", "showReference", true)
showTemplateButtonWhether the template button is shown. When unset, it follows hasTemplate of the selected skill.booleansemiDefault("AIChatInput", "showTemplateButton", false)
showUploadButtonWhether the upload button on the right is shown. Paste upload still works when false.booleansemiDefault("AIChatInput", "showUploadButton", true)
showUploadFileWhether the uploaded files area is shown; use it together with #topSlot.booleansemiDefault("AIChatInput", "showUploadFile", true)
skillHotKeyHotkey that opens the skill panel in the input.stringsemiGlobalDefault("AIChatInput", "skillHotKey")
skillsSkills. A skill holds data only; its icon goes through the #skillIcon slot.Skill[]semiGlobalDefault("AIChatInput", "skills")
suggestionsSuggestions, each a string or an object with content.Suggestion[]semiGlobalDefault("AIChatInput", "suggestions")
templatesClsClass name of the template panel.stringsemiGlobalDefault("AIChatInput", "templatesCls")
templatesStyleStyle of the template panel.StyleValuesemiGlobalDefault("AIChatInput", "templatesStyle")
topSlotPositionPosition of #topSlot relative to the reference area and the uploaded files area."top" | "bottom" | "middle"semiDefault("AIChatInput", "topSlotPosition", "top")
transformerTransform rules of custom extensions: the key is the node name, the value turns the node into a contentChange item.TypeMap<string, (obj: any) => any>semiGlobalDefault("AIChatInput", "transformer")
uploadTipPropsTooltip props of the upload tip without content; the tip content goes through the #uploadTip slot.TypeOmit<TooltipProps, "content">semiGlobalDefault("AIChatInput", "uploadTipProps")
uploadPropsUpload settings, as Upload but without fileList and listType. AIChatInput calls onChange and onRemove itself; beforeRemove may return a Promise.AIChatInputUploadPropssemiGlobalDefault("AIChatInput", "uploadProps")

Events

EventDescriptionCallback parameters
blurEmitted when the rich text input loses focus. It shadows the native blur on the root element.[event: FocusEvent]
configureChangeEmitted when the configure area changes, with the whole value and the changed value; the second argument is undefined when an item is removed.Type[value: LeftMenuChangeProps, changedValue?: LeftMenuChangeProps | undefined]
contentChangeEmitted when the input content changes, with the current content items.[contents: Content[]]
focusEmitted when the rich text input gets focus. It shadows the native focus on the root element.[event: FocusEvent]
messageSendEmitted when the send button is clicked, with the references, attachments, input contents and configure values.[message: MessageContent]
pasteEmitted on paste in the input without preventing the default behavior; read event.clipboardData for the content. It shadows the native paste on the root element.[event: ClipboardEvent]
referenceClickEmitted when a reference is clicked.[reference: Reference]
referenceDeleteEmitted when a reference is deleted. The caller owns the references and removes the item itself.[reference: Reference]
skillChangeEmitted when the skill changes; the argument is undefined when the skill is removed.[skill: Skill | undefined]
stopGenerateEmitted when the stop button is clicked.[]
uploadChangeEmitted when the uploaded file list changes.[props: UploadOnChangeProps]

Slots

SlotDescriptionScope parameters
referenceReplaces each reference, with the scope parameters item and index.AIChatInputReferenceSlotProps
topSlotCustom top area, with the references, attachments, content and delete methods as scope parameters. Placed by topSlotPosition.AIChatInputTopSlotProps
uploadButtonReplaces the upload button content while Upload keeps handling uploads and pasted files. Scope parameters: defaultNode, onOpenFileDialog, disabled, attachments.AIChatInputUploadButtonSlotProps
uploadTipContent of the upload tip.any
configureAreaFooter configure area, holding the AIChatInput.Configure items.any
actionAreaReplaces the footer action area. 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; class is the class name of the action area.AIChatInputActionAreaSlotProps
suggestionItemReplaces each suggestion. Scope parameters: suggestion, and id, class, role, aria-selected, onClick and onMouseenter to bind onto the root element.AIChatInputSuggestionItemSlotProps
skillItemReplaces each skill. Scope parameters: skill, and id, class, role, aria-selected, onClick and onMouseenter to bind onto the root element.AIChatInputSkillItemSlotProps
skillIconIcon before each skill in the skill panel, with the scope parameters item and index.AIChatInputSkillIconSlotProps
templateContent of the template panel, with the current skill and onTemplateClick, which replaces the input content with the given content.AIChatInputTemplateSlotProps

Exposes

NameDescriptionType
changeTemplateVisibleShows or hides the template layer.Type(visible: boolean) => void
deleteContentDeletes an item of the rich text, matched by uniqueKey of the content.Type(content: Content) => void
deleteUploadFileDeletes one uploaded file.Type(item: Attachment) => void
focusEditorFocuses the input, at the end by default.Type(pos?: FocusPosition | undefined) => void
getEditorReturns the tiptap editor, or null until the editor is created.Type() => Editor | null
setContentSets the input content.Type(content: Content) => void
setContentWhileSaveToolSets the input content and keeps the skill.Type(content: string) => void

class, style and undeclared attributes land on the root element. The focus, blur and paste events come from the rich text input and share their names with native events, so they shadow the native ones on the root element.

Data item types:

type Suggestion = string | { content: string; [key: string]: any };

interface Reference {
    type?: string;
    id: string;
    [key: string]: any;
}

interface Content {
    type: string;
    [key: string]: any;
}

interface MessageContent {
    references?: Reference[];
    attachments?: Attachment[];
    inputContents?: Content[];
    setup?: Setup;
}

Configure.Select, Configure.Button and Configure.RadioButton are made with getConfigureItem, and AIChatInput owns their value: they take no modelValue, field is required, initValue sets the initial value, and @change receives the item's new value.

PropertyDescriptionTypeDefault
fieldThe key of this item in the configure value, requiredstring-
initValueInitial valueSame as the wrapped component's modelValue-
@changeEmitted when the item's value changes, with the value(value: unknown) => void-

Configure.Select

Other props are the same as SelectProps

Configure.Button

Other props are the same as ButtonProps, and the icon goes through the #icon slot. The value is a boolean toggled on click, @click receives the toggled value (value: boolean) => void, and aria-pressed is rendered

Configure.RadioButton

Other props are the same as RadioGroupProps

Configure.Mcp

Props

PropertyDescriptionTypeDefault
optionsMCP options. An option holds data only; its icon goes through the #optionIcon slot.McpOption[]undefined
numThe count shown on the trigger and in the header; the number of options by default.numberundefined
showConfigureWhether the configure button is shown.booleantrue

Events

EventDescriptionCallback parameters
configureButtonClickEmitted when the configure button is clicked.[]

Slots

SlotDescriptionScope parameters
defaultReplaces the built-in menu below the header.any
optionIconIcon of each option, with the scope parameters item and index.McpOptionIconSlotProps

Exposes

NameDescriptionType
None

Undeclared attributes (the Dropdown props, class and style) fall through to the inner Dropdown and land where they do on Dropdown.

An option holds data only; its icon goes through the #optionIcon slot:

interface McpOption {
    value: string;
    label: string;
    disabled?: boolean;
    [key: string]: unknown;
}

Design Tokens

These are the component's internal Sass variables and cannot be overridden. To customize styles, override the --semi-* CSS variables (see the Design Token page) or write unlayered CSS rules.
Sass variableValueUsage
$color-aiChatInput-bordervar(--semi-color-border)AIChatInput的边框颜色
$color-aiChatInput_skill_item-bg-activevar(--semi-color-fill-0)AIChatInput技能项悬停时的背景颜色
$color-aiChatInput_suggestion_item-textvar(--semi-color-text-0)AIChatInput建议项的文本颜色
$color-aiChatInput_suggestion_item-bg-activevar(--semi-color-fill-0)AIChatInput建议项悬停时的背景颜色
$color-aiChatInput_references-textvar(--semi-color-text-2)AIChatInput引用项的文本颜色
$color-aiChatInput_reference-bgvar(--semi-color-fill-0)AIChatInput引用项的背景色
$color-aiChatInput_footer_send-textvar(--semi-white)AIChatInput底部发送按钮的内容颜色
$color-aiChatInput_footer_send-bg-defaultvar(--semi-color-primary)AIChatInput底部发送按钮的背景颜色 - 默认
$color-aiChatInput_footer_send-bg-hovervar(--semi-color-primary-hover)AIChatInput底部发送按钮的背景颜色-悬停态
$color-aiChatInput_footer_send-bg-activevar(--semi-color-primary-active)AIChatInput底部发送按钮的背景颜色-激活态
$color-aiChatInput_footer_send-bg-disabledvar(--semi-color-fill-2)AIChatInput底部发送按钮的背景颜色-禁用态
$color-aiChatInput_footer_upload-textvar(--semi-color-text-1)AIChatInput底部上传按钮的内容颜色
$color-aiChatInput_footer_upload-bg-defaulttransparentAIChatInput底部上传按钮的背景颜色 - 默认
$color-aiChatInput_footer_upload-bg-hovervar(--semi-color-fill-0)AIChatInput底部上传按钮的背景颜色-悬停态
$color-aiChatInput_footer_upload-bg-activevar(--semi-color-fill-1)AIChatInput底部上传按钮的背景颜色-悬停态
$color-aiChatInput_footer_configure_button-border-activevar(--semi-color-primary)AIChatInput底部配置区域按钮的按钮边框颜色 - 激活态
$color-aiChatInput_footer_configure_button-textvar(--semi-color-primary)AIChatInput底部配置区域按钮的按钮文字颜色 - 激活态
$color-aiChatInput_footer_configure_select-bordervar(--semi-color-border)AIChatInput底部配置区域下拉选择的按钮边框颜色
$color-aiChatInput_footer_configure_radio_button_checkedvar(--semi-color-text-1)AIChatInput底部配置区域下拉选择的单选按钮选中时的背景颜色
$color-aiChatInput_footer_configure_radio_button_checked-bgvar(--semi-color-bg-2)AIChatInput底部配置区域下拉选择的单选按钮选中时的背景颜色
$color-aiChatInput_footer-configure_mcp_header_title-textvar(--semi-color-text-2)AIChatInput底部配置区域mcp配置下拉的顶部内容的标题颜色
$color-aiChatInput_attachment-bgvar(--semi-color-fill-0)AIChatInput附件项的背景颜色
$color-aiChatInput_attachment_name-textvar(--semi-color-text-0)AIChatInput附件项内容名称的文本颜色
$color-aiChatInput_attachment_delete-bgvar(--semi-color-text-1)AIChatInput附件项内容的删除按钮的背景颜色
$color-aiChatInput_attachment_delete-iconvar(--semi-color-bg-0)AIChatInput附件项内容的删除按钮的图标颜色
$color-aiChatInput_attachment_scroll_button-bgvar(--semi-color-bg-2)AIChatInput附件项滚动按钮的背景颜色
$color-aiChatInput_attachment_scroll_buttonvar(--semi-color-text-2)AIChatInput附件项滚动按钮的背景颜色
$color-aiChatInput_rich_text-input_slot-bgvar(--semi-color-primary-light-default)AIChatInput富文本编辑器输入插槽的背景颜色
$color-aiChatInput_rich_text-input_slot-textvar(--semi-color-primary)AIChatInput富文本编辑器输入插槽的文本颜色
$color-aiChatInput_rich_text-input_slot_placeholdervar(--semi-color-primary-disabled)AIChatInput富文本编辑器输入插槽占位符的文本颜色
$color-aiChatInput_rich_text-select_slot-bgvar(--semi-color-primary-light-default)AIChatInput富文本编辑器选择器插槽的背景颜色
$color-aiChatInput_rich_text-select_selection-textvar(--semi-color-primary)AIChatInput富文本编辑器选择器插槽的选择器文本颜色
$color-aiChatInput_rich_text-select_slot_arrowvar(--semi-color-text-2)AIChatInput富文本编辑器选择器插槽的下拉箭头颜色
$color-aiChatInput_rich_text-skill_slot-textvar(--semi-color-primary)AIChatInput富文本编辑器技能插槽的文本颜色
$color-aiChatInput_rich_text-skill_slot-bg-hovervar(--semi-color-primary-light-default)AIChatInput富文本编辑器技能插槽的背景色 - 悬停态
$color-aiChatInput_rich_text-skill_slot_delete-bgvar(--semi-color-text-1)AIChatInput富文本编辑器技能插槽的删除按钮的背景颜色
$color-aiChatInput_rich_text-skill_slot_delete-textvar(--semi-color-bg-0)AIChatInput富文本编辑器技能插槽的删除按钮的背景颜色
$color-aiChatInput_ref_icon_word-bgvar(--semi-blue-4)AIChatInput 引用/上传内容为文本文件时的图标背景色
$color-aiChatInput_ref_icon_word-pdfvar(--semi-red-4)AIChatInput 引用/上传内容为 pdf 时的图标背景色
$color-aiChatInput_ref_icon_word-codevar(--semi-teal-5)AIChatInput 引用/上传内容为代码的图标背景色
$color-aiChatInput_ref_icon_word-excelvar(--semi-green-5)AIChatInput 引用/上传内容为表格类型的图标背景色
$color-aiChatInput_ref_icon_word-videovar(--semi-purple-4)AIChatInput 引用/上传内容为视频类型的图标背景色
$color-aiChatInput_ref_icon_word-audiovar(--semi-purple-4)AIChatInput 引用/上传内容为音频类型的图标背景色
$color-aiChatInput_ref_icon_word-unknownvar(--semi-grey-5)AIChatInput 引用/上传内容为未知类型的图标背景色
$color-aiChatInput_reference_delete-bgvar(--semi-color-fill-1)AIChatInput引用项删除图标的背景色 - 悬停态
$color-aiChatInput_footer_configure_select-textvar(--semi-color-text-1)AIChatInput底部配置区域下拉选择的按钮文字颜色
$color-aiChatInput_footer_configure_select-textvar(--semi-color-text-1)AIChatInput底部配置区域下拉选择的按钮文字颜色
$spacing-aiChatInput-padding20pxAIChatInput的外边距
$spacing-aiChatInput_skill-paddingY4pxAIChatInput技能面板的垂直内边距
$spacing-aiChatInput_skill-paddingX0pxAIChatInput技面板的水平内边距
$spacing-aiChatInput_skill_item-columnGap8pxAIChatInput技能项内容的水平间距
$spacing-aiChatInput_skill_item-paddingY8pxAIChatInput技能项的垂直内边距
$spacing-aiChatInput_skill_item-paddingX20pxAIChatInput技能项的水平内边距
$spacing-aiChatInput_suggestion_item-paddingY8pxAIChatInput建议项的垂直内边距
$spacing-aiChatInput_suggestion_item-paddingX20pxAIChatInput建议项的水平内边距
$spacing-aiChatInput_references-marginBottom8pxAIChatInput引用项的底部外边距
$spacing-aiChatInput_references-columnGap4pxAIChatInput技能块内容的水平间距
$spacing-aiChatInput_references-rowGap4pxAIChatInput技能块内容的垂直间距
$spacing-aiChatInput_reference-paddingY8pxAIChatInput引用项的垂直内边距
$spacing-aiChatInput_reference-paddingX12pxAIChatInput引用项的水平内边距
$spacing-aiChatInput_reference-columnGap8pxAIChatInput引用项内容的水平间距
$spacing-aiChatInput_footer-marginTop20pxAIChatInput引用项的顶部外边距
$spacing-aiChatInput_footer_action-columnGap8pxAIChatInput底部右侧操作区域项目的水平间距
$spacing-aiChatInput_footer_configure-columnGap8pxAIChatInput底部配置区域项目的水平间距
$spacing-aiChatInput_footer_configure_radio_button-padding4pxAIChatInput底部配置区域下拉选择的单选按钮的内边距
$spacing-aiChatInput_footer_configure_mcp_header-paddingTop8pxAIChatInput底部配置区域mcp配置下拉的顶部内容的上外边距
$spacing-aiChatInput_footer_configure_mcp_header-paddingBottom0pxAIChatInput底部配置区域mcp配置下拉的顶部内容的下外边距
$spacing-aiChatInput_footer_configure_mcp_header-paddingX16pxAIChatInput底部配置区域mcp配置下拉的顶部内容的水平外边距
$spacing-aiChatInput_footer_configure_mcp_columnGap4pxAIChatInput底部配置区域mcp配置下拉的顶部内容的水平间距
$spacing-aiChatInput_attachment-columnGap8pxAIChatInput附件项的水平间距
$spacing-aiChatInput_attachment-padding8pxAIChatInput附件项的内间距
$spacing-aiChatInput_attachment_content_size-columnGap4pxAIChatInput附件项类型和大小的间距
$spacing-aiChatInput_attachment_scroll_wrapper-marginBottom8pxAIChatInput附件项滚动区域的底部外边距
$spacing-aiChatInput_attachment_scroll_container8pxAIChatInput附件项滚动区域的内间距
$spacing-aiChatInput_attachment_scroll_button_offsetX10pxAIChatInput附件项滚动按钮的和滚动区域的水平距离
$spacing-aiChatInput_rich_text-input_slot-paddingY2pxAIChatInput富文本编辑器输入插槽的垂直内边距
$spacing-aiChatInput_rich_text-input_slot-paddingX4pxAIChatInput富文本编辑器输入插槽的水平内边距
$spacing-aiChatInput_rich_text-input_slot-marginY2pxAIChatInput富文本编辑器输入插槽的垂直内边距
$spacing-aiChatInput_rich_text-input_slot-marginX2pxAIChatInput富文本编辑器输入插槽的水平内边距
$spacing-aiChatInput_rich_text-select_slot-paddingX4pxAIChatInput富文本编辑器选择器插槽的水平内边距
$spacing-aiChatInput_rich_text-select_slot-paddingY2pxAIChatInput富文本编辑器选择器插槽的垂直内边距
$spacing-aiChatInput_rich_text-select_slot-marginX4pxAIChatInput富文本编辑器选择器插槽的垂直外边距
$spacing-aiChatInput_rich_text-select_slot-marginY2pxAIChatInput富文本编辑器选择器插槽的水平外边距
$spacing-aiChatInput_rich_text-select_selection-marginLeft0pxAIChatInput富文本编辑器选择器插槽的选择器文本的左侧外边距
$spacing-aiChatInput_rich_text-skill_slot-paddingX4pxAIChatInput富文本编辑器技能插槽的水平内边距
$spacing-aiChatInput_rich_text-skill_slot-paddingY0pxAIChatInput富文本编辑器技能插槽的垂直内边距
$spacing-aiChatInput_rich_text-skill_slot-marginX2pxAIChatInput富文本编辑器技能插槽的水平外边距
$spacing-aiChatInput_rich_text-skill_slot-marginY2pxAIChatInput富文本编辑器技能插槽的垂直外边距
$spacing-aiChatInput_rich_text-skill_slot-top0pxAIChatInput富文本编辑器技能插槽删除按钮距离顶部距离
$spacing-aiChatInput_rich_text-skill_slot-right0pxAIChatInput富文本编辑器技能插槽删除按钮距离右侧距离
$spacing-aiChatInput_reference_icon-marginRight4pxAIChatInput 引用项的图标距离右侧距离
$spacing-aiChatInput_references_delete-padding2pxAIChatInput引用项删除图标的内间距
$width-aiChatInput-border1pxAIChatInput的边框宽度
$width-aiChatInput_footer_action_button32pxAIChatInput底部右侧操作按钮的宽度
$height-aiChatInput_footer_action_button32pxAIChatInput底部右侧操作按钮的高度
$width-aiChatInput_footer_configure_button-border1pxAIChatInput底部配置区域按钮的边框宽度
$width-aiChatInput_footer_configure_select-border1pxAIChatInput底部配置区域下拉选择的边框宽度
$height-aiChatInput_footer_configure_mcp_header16pxAIChatInput底部配置区域mcp配置下拉的顶部内容的高度
$width-aiChatInput_attachment240pxAIChatInput附件项的宽度(含内边距)
$height-aiChatInput_attachment_left36pxAIChatInput附件项左侧图标/图片的宽高
$width-aiChatInput_attachment-content180pxAIChatInput附件项内容的宽度
$height-aiChatInput_attachment-content20pxAIChatInput附件项内容的高度
$width-aiChatInput_attachment_delete16pxAIChatInput附件项内容的删除按钮的宽度
$width-aiChatInput_attachment_scroll_button16pxAIChatInput附件项滚动按钮的宽度
$width-aiChatInput_attachment_scroll_button_icon12pxAIChatInput附件项滚动按钮的图标大小
$height-aiChatInput_rich_text-select-slot24pxAIChatInput富文本编辑器选择器插槽的高度
$width-aiChatInput_rich_text-select_slot_arrow16pxAIChatInput富文本编辑器选择器插槽的下拉箭头宽度
$width-aiChatInput_rich_text-select_slot_delete12pxAIChatInput富文本编辑器选择器插槽的删除按钮的宽高
$width-aiChatInput_rich_text-select_slot_delete_icon8pxAIChatInput富文本编辑器选择器插槽的删除按钮icon的宽高
$width-aiChatInput_rich_text-input_slot2pxAIChatInput富文本编辑器输入插槽的最小宽度
$width-aiChatInput_reference_icon16pxAIChatInput 引用项的图标宽度
$radius-aiChatInput12pxAIChatInput输入框的圆角
$radius-aiChatInput_skill8pxAIChatInput技能块的圆角
$radius-aiChatInput_reference6pxAIChatInput引用项的圆角
$radius-aiChatInput_footer_round9999pxAIChatInput底部操作项目全圆角模式的圆角
$radius-aiChatInput_footer_action_button8pxAIChatInput底部右侧操作按钮的圆角
$radius-aiChatInput_attachment6pxAIChatInput附件项的圆角
$radius-aiChatInput_rich_text-input_slot4pxAIChatInput富文本编辑器输入插槽的圆角
$radius-aiChatInput_rich_text-select_slot4pxAIChatInput富文本编辑器选择器插槽的圆角
$radius-aiChatInput_rich_text-skill_slot4pxAIChatInput富文本编辑器技能插槽的圆角
$radius-aiChatInput_reference_icon2pxAIChatInput 引用项的图标圆角
$radius-aiChatInput_suggestion_item6pxAIChatInput 建议项的圆角
$font-aiChatInput_footer_configure_select-fontWeight600AIChatInput底部配置区域下拉选择的字体粗细
$font-aiChatInput_attachment_content_name-fontWeight600AIChatInput附件项内容的名称字体粗细
$font-aiChatInput_attachment_content_delete-fontSize12pxAIChatInput附件项内容的删除按钮的图标大小
$font-aiChatInput_rich_text-fontSize$font-size-regularAIChatInput富文本编辑器文字大小
$font-aiChatInput_rich_text-lineHeight24pxAIChatInput富文本编辑器文本行高
$font-aiChatInput_rich_text-input_slot-fontSize$font-size-regularAIChatInput富文本编辑器输入插槽文字大小
$font-aiChatInput_rich_text-input_slot-lineHeight20pxAIChatInput富文本编辑器输入框插槽文本行高
$font-aiChatInput_rich_text-input_slot-fontWeight600AIChatInput富文本编辑器输入插槽文本字重
$font-aiChatInput_rich_text-skill_slot-fontWeight600AIChatInput富文本编辑器技能插槽文本字重
$font-aiChatInput_footer_configure_radio_button-fontSize14pxAIChatInput底部配置区域下拉选择的单选按钮的文字大小
$z-aiChatInput_attachment_scroll_button10AIChatInput附件项滚动按钮的z-index
$z-aiChatInput_rich_text-input_slot_placeholder1AIChatInput富文本编辑器输入插槽占位符的z-index
$shadow-aiChatInput_attachment_scroll_buttonvar(--semi-shadow-elevated)AIChatInput附件项滚动按钮的阴影