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 exceptfileListandlistType; pass Upload's events as listener props such asonSuccessandonError@uploadChange: Emitted when file upload changes- When deleting uploaded files,
uploadProps.onRemovewill be called, anduploadProps.beforeRemovewill be respected (Promise supported) placeholder: Placeholder for the input boxdefaultContent: 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.
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.
#referenceslot: 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.
#configureAreaslot: Custom content for config area buttons.- Use
AIChatInput.Configurecomponents such asSelect,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.
skillssample 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:
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
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
<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
| 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 | semiGlobalDefault("AIChatInput", "canSend") |
| clearContentOnGenerating | Whether 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). | boolean | semiDefault("AIChatInput", "clearContentOnGenerating", true) |
| defaultContent | Default content of the input, an html string or JSON like Tiptap's Content. Read once, when the editor is created. | Content | semiGlobalDefault("AIChatInput", "defaultContent") |
| dropdownMatchTriggerWidth | Whether the dropdown layer has the same width as the input. | boolean | semiDefault("AIChatInput", "dropdownMatchTriggerWidth", true) |
| extensions | Custom tiptap extensions. Read once, when the editor is created; later changes have no effect. | Extensions | semiGlobalDefault("AIChatInput", "extensions") |
| generating | Whether a response is being generated. While true, the send button becomes the stop button. | boolean | semiDefault("AIChatInput", "generating", false) |
| keepSkillAfterSend | Whether the selected skill is kept after a message is sent. | boolean | semiDefault("AIChatInput", "keepSkillAfterSend", false) |
| placeholder | Placeholder of the input: a string, or a function that returns a string for a node. | PlaceholderProps | undefined |
| popoverProps | Props of the dropdown layer. The class is merged with the internal class names and the style with the panel width. | AIChatInputPopoverProps | semiGlobalDefault("AIChatInput", "popoverProps") |
| references | References shown with the input. | Reference[] | semiGlobalDefault("AIChatInput", "references") |
| round | Whether the buttons of the footer configure and action areas are fully rounded. | boolean | semiDefault("AIChatInput", "round", true) |
| sendHotKey | Keyboard 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") |
| showPlaceholderWhenSkillOnly | Whether the placeholder shows when only a skill is selected, after the skill. Read once, when the editor is created. | boolean | semiDefault("AIChatInput", "showPlaceholderWhenSkillOnly", false) |
| showReference | Whether the reference area is shown; use it together with #topSlot. | boolean | semiDefault("AIChatInput", "showReference", true) |
| showTemplateButton | Whether the template button is shown. When unset, it follows hasTemplate of the selected skill. | boolean | semiDefault("AIChatInput", "showTemplateButton", false) |
| showUploadButton | Whether the upload button on the right is shown. Paste upload still works when false. | boolean | semiDefault("AIChatInput", "showUploadButton", true) |
| showUploadFile | Whether the uploaded files area is shown; use it together with #topSlot. | boolean | semiDefault("AIChatInput", "showUploadFile", true) |
| skillHotKey | Hotkey that opens the skill panel in the input. | string | semiGlobalDefault("AIChatInput", "skillHotKey") |
| skills | Skills. A skill holds data only; its icon goes through the #skillIcon slot. | Skill[] | semiGlobalDefault("AIChatInput", "skills") |
| suggestions | Suggestions, each a string or an object with content. | Suggestion[] | semiGlobalDefault("AIChatInput", "suggestions") |
| templatesCls | Class name of the template panel. | string | semiGlobalDefault("AIChatInput", "templatesCls") |
| templatesStyle | Style of the template panel. | StyleValue | semiGlobalDefault("AIChatInput", "templatesStyle") |
| topSlotPosition | Position of #topSlot relative to the reference area and the uploaded files area. | "top" | "bottom" | "middle" | semiDefault("AIChatInput", "topSlotPosition", "top") |
| transformer | Transform 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") |
| uploadTipProps | Tooltip props of the upload tip without content; the tip content goes through the #uploadTip slot. | TypeOmit<TooltipProps, "content"> | semiGlobalDefault("AIChatInput", "uploadTipProps") |
| uploadProps | Upload settings, as Upload but without fileList and listType. AIChatInput calls onChange and onRemove itself; beforeRemove may return a Promise. | AIChatInputUploadProps | semiGlobalDefault("AIChatInput", "uploadProps") |
Events
| Event | Description | Callback parameters |
|---|---|---|
| blur | Emitted when the rich text input loses focus. It shadows the native blur on the root element. | [event: FocusEvent] |
| configureChange | Emitted 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] |
| contentChange | Emitted when the input content changes, with the current content items. | [contents: Content[]] |
| focus | Emitted when the rich text input gets focus. It shadows the native focus on the root element. | [event: FocusEvent] |
| messageSend | Emitted when the send button is clicked, with the references, attachments, input contents and configure values. | [message: MessageContent] |
| paste | Emitted 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] |
| referenceClick | Emitted when a reference is clicked. | [reference: Reference] |
| referenceDelete | Emitted when a reference is deleted. The caller owns the references and removes the item itself. | [reference: Reference] |
| skillChange | Emitted when the skill changes; the argument is undefined when the skill is removed. | [skill: Skill | undefined] |
| stopGenerate | Emitted when the stop button is clicked. | [] |
| uploadChange | Emitted when the uploaded file list changes. | [props: UploadOnChangeProps] |
Slots
| Slot | Description | Scope parameters |
|---|---|---|
| reference | Replaces each reference, with the scope parameters item and index. | AIChatInputReferenceSlotProps |
| topSlot | Custom top area, with the references, attachments, content and delete methods as scope parameters. Placed by topSlotPosition. | AIChatInputTopSlotProps |
| uploadButton | Replaces the upload button content while Upload keeps handling uploads and pasted files. Scope parameters: defaultNode, onOpenFileDialog, disabled, attachments. | AIChatInputUploadButtonSlotProps |
| uploadTip | Content of the upload tip. | any |
| configureArea | Footer configure area, holding the AIChatInput.Configure items. | any |
| actionArea | Replaces 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 |
| suggestionItem | Replaces each suggestion. Scope parameters: suggestion, and id, class, role, aria-selected, onClick and onMouseenter to bind onto the root element. | AIChatInputSuggestionItemSlotProps |
| skillItem | Replaces each skill. Scope parameters: skill, and id, class, role, aria-selected, onClick and onMouseenter to bind onto the root element. | AIChatInputSkillItemSlotProps |
| skillIcon | Icon before each skill in the skill panel, with the scope parameters item and index. | AIChatInputSkillIconSlotProps |
| template | Content of the template panel, with the current skill and onTemplateClick, which replaces the input content with the given content. | AIChatInputTemplateSlotProps |
Exposes
| Name | Description | Type |
|---|---|---|
| changeTemplateVisible | Shows or hides the template layer. | Type(visible: boolean) => void |
| deleteContent | Deletes an item of the rich text, matched by uniqueKey of the content. | Type(content: Content) => void |
| deleteUploadFile | Deletes one uploaded file. | Type(item: Attachment) => void |
| focusEditor | Focuses the input, at the end by default. | Type(pos?: FocusPosition | undefined) => void |
| getEditor | Returns the tiptap editor, or null until the editor is created. | Type() => Editor | null |
| setContent | Sets the input content. | Type(content: Content) => void |
| setContentWhileSaveTool | Sets 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.
| 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 modelValue | - |
| @change | Emitted 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
| Property | Description | Type | Default |
|---|---|---|---|
| options | MCP options. An option holds data only; its icon goes through the #optionIcon slot. | McpOption[] | undefined |
| num | The count shown on the trigger and in the header; the number of options by default. | number | undefined |
| showConfigure | Whether the configure button is shown. | boolean | true |
Events
| Event | Description | Callback parameters |
|---|---|---|
| configureButtonClick | Emitted when the configure button is clicked. | [] |
Slots
| Slot | Description | Scope parameters |
|---|---|---|
| default | Replaces the built-in menu below the header. | any |
| optionIcon | Icon of each option, with the scope parameters item and index. | McpOptionIconSlotProps |
Exposes
| Name | Description | Type |
|---|---|---|
| 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
| 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附件项滚动按钮的阴影 |