复制代码
在 AI 聊天应用中,输入框是用户与 AI 之间最重要的交互界面。一个优秀的输入组件,不仅是文本的"收容所",更是连接用户意图与 AI 能力的桥梁。无论模型能力多么强大,如果用户无法精准、高效地表达自己的想法,整个 AI 体验都会大打折扣。

传统的 <input> 或 <textarea> 已经无法满足现代 AI 应用的复杂需求。用户需要:
TinyRobot Sender 正是为此而生 —— 它是一个高度可组合、可扩展的 AI 聊天输入组件,基于 Tiptap 富文本编辑器构建,提供企业级的输入体验。
Sender 支持单行 (single) 和多行 (multiple) 两种输入模式。单行模式下内容超出宽度时会自动切换为多行,无需用户手动调整。
复制代码<script setup lang="ts">
import { ref } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'const content = ref('')
const mode = ref<'single' | 'multiple'>('single')const handleSubmit = (text: string) => {
console.log('提交内容:', text)
content.value = ''
}
</script><template>
<div>
<div style="display: flex; gap: 12px; margin-bottom: 16px;">
<button
:style="{ background: mode === 'single' ? '#1476ff' : '#f0f0f0', color: mode === 'single' ? '#fff' : '#333' }"
@click="mode = 'single'"
>
单行模式
</button>
<button
:style="{ background: mode === 'multiple' ? '#1476ff' : '#f0f0f0', color: mode === 'multiple' ? '#fff' : '#333' }"
@click="mode = 'multiple'"
>
多行模式
</button>
</div> <TrSender
v-model="content"
:mode="mode"
placeholder="请输入消息..."
:max-length="200"
show-word-limit
clearable
@submit="handleSubmit"
/>
</div>
</template>
核心 Props 说明:
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
modelValue / v-model | 输入框内容绑定 | string | '' |
mode | 输入模式 | 'single' | 'multiple' | 'single' |
disabled | 是否禁用 | boolean | false |
loading | 加载状态(显示停止按钮) | boolean | false |
maxLength | 最大输入长度 | number | Infinity |
showWordLimit | 显示字数统计 | boolean | false |
size | 组件尺寸 | 'normal' | 'small' | 'normal' |
Sender 的核心设计理念是可插拔的扩展架构。通过 extensions prop,你可以按需组合 Template、Mention、Suggestion 等功能扩展,每个扩展互相独立、自由组合。
复制代码// 扩展就是普通的数组元素
const extensions = [
TrSender.template(templateData),
TrSender.mention(mentionItems),
TrSender.suggestion(suggestionItems, { filterFn: customFilter })
]// 通过 props 传入
<TrSender :extensions="extensions" />
提供两种集成方式:
TrSender.template()、TrSender.mention()、TrSender.suggestion(),用于简单场景TrSender.Mention.configure({...}),用于复杂配置(如自定义 onSelect 回调、filterFn 等)Template 扩展让用户通过预设模板快速填写结构化 Prompt。支持 text(固定文本)、block(可编辑字段)、select(下拉选择器)三种节点类型。
复制代码<script setup lang="ts">
import { ref } from 'vue'
import { Button } from '@opentiny/vue'
import { TrSender } from '@opentiny/tiny-robot'
import type { TemplateItem, StructuredData } from '@opentiny/tiny-robot'const content = ref('')
const templateData = ref<TemplateItem[]>([])// 便捷函数:传入响应式 ref,数据变化时自动同步编辑器
const extensions = [TrSender.template(templateData)]const setTemplate = () => {
templateData.value = [
{ type: 'text', content: '请帮我写一份关于' },
{ type: 'block', content: '人工智能' },
{ type: 'text', content: '的' },
{ type: 'block', content: '技术报告' },
{ type: 'text', content: ',字数要求' },
{ type: 'block', content: '3000字' },
{ type: 'text', content: ',风格要求' },
{
type: 'select',
placeholder: '选择风格',
content: '',
options: [
{ label: '正式学术', value: 'formal_academic' },
{ label: '通俗易懂', value: 'easy_reading' },
{ label: '技术深度', value: 'technical_deep' },
]
},
{ type: 'text', content: '。' },
]
}const handleSubmit = (text: string, data?: StructuredData) => {
console.log('纯文本:', text)
// data 结构:
// [
// { type: 'text', content: '请帮我写一份关于' },
// { type: 'block', content: '人工智能' },
// { type: 'text', content: '的技术报告...' }
// ]
}
</script><template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<Button size="small" @click="setTemplate">插入 Prompt 模板</Button>
<TrSender
v-model="content"
:extensions="extensions"
mode="multiple"
placeholder="点击按钮插入模板..."
:max-length="500"
show-word-limit
clearable
@submit="handleSubmit"
/>
</div>
</template>
Template 通过响应式 ref 机制,当 templateData.value 发生变化时,编辑器内容会自动更新,光标也会自动聚焦到第一个可编辑字段,无需手动调用任何方法。
Mention 扩展实现了类似社交平台的 @提及功能。输入触发字符(默认 @)会弹出选择列表,支持键盘导航(↑↓)和搜索过滤。
复制代码<script setup lang="ts">
import { ref } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'
import type { MentionItem, StructuredData } from '@opentiny/tiny-robot'const content = ref('')const assistants: MentionItem[] = [
{ label: '代码助手', value: 'you_are_a_coding_expert' },
{ label: '文案大师', value: 'you_are_a_copywriter' },
{ label: '数据分析师', value: 'you_are_a_data_analyst' },
{ label: '翻译专家', value: 'you_are_a_translator' },
]// 便捷函数:默认触发字符为 @
const extensions = [TrSender.mention(assistants, '@')]// 也可以自定义触发字符,如用 # 替代 @
// const extensions = [TrSender.mention(hashtags, '#')]const handleSubmit = (text: string, data?: StructuredData) => {
// text: "帮我分析 @代码助手 返回的代码"
// data: [
// { type: 'text', content: '帮我分析 ' },
// { type: 'mention', content: '代码助手', value: 'you_are_a_coding_expert' },
// { type: 'text', content: ' 返回的代码' }
// ] // 提取所有提及的助手
const mentions = data?.filter(item => item.type === 'mention') || []
const assistantIds = mentions.map(m => m.value)
console.log('提及的助手:', assistantIds)
}
</script><template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<p style="color: #666; font-size: 14px; margin: 0;">
输入 <code>@</code> 触发提及选择,支持键盘导航和搜索过滤
</p>
<TrSender
v-model="content"
:extensions="extensions"
placeholder="输入 @ 选择助手..."
mode="multiple"
:max-length="500"
show-word-limit
clearable
@submit="handleSubmit"
/>
</div>
</template>
Mention 扩展支持三个配置项:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
items | MentionItem[] | Ref<MentionItem[]> | [] | 提及项列表,支持响应式 ref |
char | string | '@' | 触发字符 |
allowSpaces | boolean | false | 是否允许触发字符后输入空格 |
Suggestion 扩展提供智能输入联想功能。用户输入时自动弹出建议列表,支持键盘导航和 Tab 自动补全。
复制代码<script setup lang="ts">
import { ref, computed } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'
import type { SenderSuggestionItem } from '@opentiny/tiny-robot'const input = ref('')const suggestions: SenderSuggestionItem[] = [
{ content: 'ECS-云服务器卡顿排查' },
{ content: 'ECS-备份弹性云服务器' },
{ content: 'ECS-实例无法启动处理' },
{ content: 'CDN-权限管理配置指南' },
{ content: 'CDN-缓存刷新问题排查' },
{ content: 'OSS-存储桶访问控制' },
]// 标准配置:使用自定义 filterFn 实现按分类前缀匹配
const extensions = computed(() => [
TrSender.Suggestion.configure({
items: suggestions,
// 自定义过滤逻辑
filterFn: (items: SenderSuggestionItem[], query: string) => {
if (!query) return []
const lowerQuery = query.toLowerCase()
// 按 - 前面的分类标签匹配
return items.filter((item) => {
const category = item.content.split('-')[0].toLowerCase()
return category.startsWith(lowerQuery)
})
},
showAutoComplete: true,
popupWidth: 500,
onSelect: (item) => {
console.log('选中建议:', item.content)
},
}),
])
</script><template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<p style="color: #666; font-size: 14px; margin: 0;">
输入 ECS、CDN 或 OSS 查看分类联想,Tab 键快速补全
</p>
<TrSender
v-model="input"
:extensions="extensions"
placeholder="输入 ECS 或 CDN 查看建议..."
@submit="(text) => console.log('提交:', text)"
/>
</div>
</template>
Suggestion 配置项详解:
| 配置项 | 说明 | 类型 | 默认值 |
|---|---|---|---|
items | 建议项列表 | SenderSuggestionItem[] | [] |
filterFn | 自定义过滤函数,不传则不过滤 | (items, query) => items | undefined |
showAutoComplete | 显示灰色补全提示 | boolean | true |
activeSuggestionKeys | 激活建议的按键 | string[] | ['Enter'] |
popupWidth | 弹窗宽度(支持数字/百分比) | number | string | 400 |
onSelect | 选中回调,返回 false 阻止默认回填 | (item) => void | false | - |
VoiceButton 是一个独立的语音输入按钮组件,通过 footer 或 footer-right 插槽集成到 Sender 中。它同时支持浏览器内置的 Web Speech API 和自定义第三方语音识别服务。
复制代码<script setup lang="ts">
import { ref } from 'vue'
import { TrSender, VoiceButton } from '@opentiny/tiny-robot'const content = ref('')
const voiceMode = ref<'mixed' | 'continuous'>('mixed')const handleSubmit = (text: string) => {
console.log('提交内容:', text)
content.value = ''
}
</script><template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; gap: 12px; align-items: center;">
<span>语音模式:</span>
<label><input type="radio" v-model="voiceMode" value="mixed" /> 混合输入</label>
<label><input type="radio" v-model="voiceMode" value="continuous" /> 连续识别</label>
</div>
<p style="color: #666; font-size: 13px; margin: 0;">
{{ voiceMode === 'mixed'
? '识别结果追加到输入框,可继续编辑修改'
: '持续识别并自动替换内容,适合长段语音输入' }}
</p> <!-- VoiceButton 通过 key 在模式切换时重新实例化 -->
<TrSender
:key="voiceMode"
v-model="content"
mode="multiple"
@submit="handleSubmit"
>
<template #footer-right>
<VoiceButton
:speech-config="voiceMode === 'mixed'
? { autoReplace: false, interimResults: true }
: { autoReplace: true, continuous: true }"
tooltip="语音输入"
@speech-final="(transcript) => { content += transcript }"
/>
</template>
</TrSender>
</div>
</template>
VoiceButton 核心配置:
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
speechConfig.autoReplace | 是否自动替换内容(false 则追加) | boolean | true |
speechConfig.continuous | 是否持续识别 | boolean | false |
speechConfig.interimResults | 是否返回中间结果 | boolean | false |
speechConfig.lang | 识别语言 | string | 浏览器默认语言 |
autoInsert | 是否自动插入识别结果到编辑器 | boolean | true |
如需集成阿里云、百度、Azure 等第三方语音服务,通过 speechConfig.customHandler 传入自定义处理器即可。
UploadButton 允许用户上传文件(图片、文档等)作为对话的上下文输入,支持文件类型过滤、大小限制和数量限制。
复制代码<script setup lang="ts">
import { ref } from 'vue'
import { TrSender, UploadButton } from '@opentiny/tiny-robot'const content = ref('')
const uploadedFiles = ref<File[]>([])const handleSubmit = (text: string) => {
console.log('提交文本:', text)
console.log('附带文件:', uploadedFiles.value)
content.value = ''
uploadedFiles.value = []
}const handleFilesSelected = (files: File[]) => {
uploadedFiles.value = files
// 可以在这里做上传逻辑或直接作为附件信息
}
</script><template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<TrSender
v-model="content"
mode="multiple"
placeholder="输入问题,或上传相关文件..."
clearable
@submit="handleSubmit"
>
<template #footer-right>
<UploadButton
accept="image/*,.pdf,.doc,.docx"
:multiple="true"
:max-size="10"
:max-count="5"
tooltip="上传文件(最多5个,每个≤10MB)"
tooltip-placement="top"
@select="handleFilesSelected"
/>
</template>
</TrSender> <div v-if="uploadedFiles.length" style="font-size: 13px; color: #666;">
已选择:{{ uploadedFiles.map(f => f.name).join(', ') }}
</div>
</div>
</template>
UploadButton 核心配置:
| 属性 | 说明 | 类型 | 默认值 |
|---|---|---|---|
accept | 接受的 MIME 类型 | string | '*' |
multiple | 是否支持多选 | boolean | false |
maxSize | 文件大小限制(MB) | number | - |
maxCount | 最大文件数量 | number | - |
tooltip | 悬停提示文本 | string | - |
Sender 提供多个插槽位置以支持灵活的布局扩展:
| 插槽 | 位置 | 作用域 | 典型用途 |
|---|---|---|---|
header | 输入框上方 | - | 标题、提示信息 |
prefix | 输入框内部左侧 | - | AI 图标、模型选择 |
content | 编辑器内容区域 | { editor } | 完全自定义编辑器内容 |
actions-inline | 单行模式操作区 | - | 单行模式下的操作按钮 |
footer | 底部左侧 | { editor, hasContent, disabled, loading } | 增强功能按钮 |
footer-right | 底部右侧 | - | 上传、语音按钮 |
复制代码<script setup lang="ts">
import { ref } from 'vue'
import { TrSender, UploadButton, VoiceButton } from '@opentiny/tiny-robot'const content = ref('')
const deepThinking = ref(false)const handleSubmit = (text: string) => {
console.log('提交:', text, deepThinking.value ? '(深度思考模式)' : '')
content.value = ''
}
</script><template>
<TrSender
v-model="content"
placeholder="输入你的问题..."
mode="multiple"
clearable
@submit="handleSubmit"
>
<!-- 顶部标题 -->
<template #header>
<div style="text-align: center; font-weight: 700; padding: 8px 0;">
AI 智能助手
</div>
</template> <!-- 输入框前缀图标 -->
<template #prefix>
<span style="font-size: 22px; margin-right: 8px;"></span>
</template> <!-- 底部左侧功能按钮 -->
<template #footer="{ editor, hasContent }">
<button
:style="{
padding: '4px 12px', borderRadius: '16px', border: '1px solid #e0e0e0',
background: deepThinking ? '#1476ff' : 'transparent',
color: deepThinking ? '#fff' : '#666', cursor: 'pointer', fontSize: '13px'
}"
@click="deepThinking = !deepThinking"
>
{{ deepThinking ? ' 深度思考' : ' 深度思考' }}
</button>
<span v-if="hasContent" style="font-size: 12px; color: #999; margin-left: 8px;">
已输入 {{ content.length }} 字
</span>
</template> <!-- 底部右侧功能按钮 -->
<template #footer-right>
<UploadButton
accept="image/*"
tooltip="上传图片"
tooltip-placement="top"
@select="(files) => console.log('选择文件:', files)"
/>
<VoiceButton
:speech-config="{ autoReplace: false, interimResults: true }"
tooltip="语音输入"
tooltip-placement="top"
/>
</template>
</TrSender>
</template>
不同的应用场景需要不同的提交方式。Sender 通过 submitType 属性提供三种方式:
复制代码<script setup lang="ts">
import { ref } from 'vue'
import { TrSender } from '@opentiny/tiny-robot'const content = ref('')
const submitType = ref<'enter' | 'ctrlEnter' | 'shiftEnter'>('ctrlEnter')const handleSubmit = (text: string) => {
console.log('提交:', text)
content.value = ''
}
</script><template>
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; gap: 16px; align-items: center;">
<span>提交方式:</span>
<label><input type="radio" v-model="submitType" value="enter" /> Enter 提交</label>
<label><input type="radio" v-model="submitType" value="ctrlEnter" /> Ctrl+Enter 提交</label>
<label><input type="radio" v-model="submitType" value="shiftEnter" /> Shift+Enter 提交</label>
</div> <TrSender
v-model="content"
:submit-type="submitType"
placeholder="输入内容后使用对应快捷键提交..."
@submit="handleSubmit"
/>
</div>
</template>
快捷键行为对照表:
| submitType | 提交快捷键 | 换行快捷键 |
|---|---|---|
enter | Enter | Ctrl+Enter / Shift+Enter |
ctrlEnter | Ctrl+Enter | Enter |
shiftEnter | Shift+Enter | Enter |
Sender 的 submit 事件不仅返回纯文本,当使用了 Template 或 Mention 扩展时,还会返回结构化的 data 参数,让你能够精确提取用户的意图。
复制代码// submit 事件签名
@submit="(text: string, data?: StructuredData) => { ... }"
复制代码const handleSubmit = (text: string, data?: StructuredData) => {
// 用户输入: "帮我分析 @代码助手 的代码质量"
// text: "帮我分析 @代码助手 的代码质量"
// data: [
// { type: 'text', content: '帮我分析 ' },
// { type: 'mention', content: '代码助手', value: 'you_are_a_coding_expert' },
// { type: 'text', content: ' 的代码质量' }
// ] // 提取提及的助手 ID 列表
const mentions = data?.filter(item => item.type === 'mention') || []
const assistantIds = mentions.map(item => item.value) // 自定义 Slack 风格格式
const customText = data?.map(item =>
item.type === 'mention' ? `<@${item.value}>` : item.content
).join('')
}
复制代码const handleSubmit = (text: string, data?: StructuredData) => {
// 用户通过模板输入后提交
// text: "请帮我写一份关于人工智能的技术报告,字数要求3000字"
// data: [
// { type: 'text', content: '请帮我写一份关于' },
// { type: 'block', content: '人工智能' },
// { type: 'text', content: '的技术报告...' }
// ] // 提取所有可编辑块的值
const blocks = data?.filter(item => item.type === 'block') || []
const blockValues = blocks.map(b => b.content)
}
这种结构化设计让你可以轻松实现:
Sender 在 v0.4 版本进行了重大重构,底层从传统 textarea 升级为基于 Tiptap 的富文本编辑器,带来了更强大的扩展能力和更灵活的插槽系统。
TinyRobot 提供了 SenderCompat 兼容组件作为过渡方案,只需修改一行导入:
复制代码// 旧代码 (v0.3.x)
import { TrSender } from '@opentiny/tiny-robot'// 使用 SenderCompat 快速迁移
import { TrSenderCompat as TrSender } from '@opentiny/tiny-robot'
推荐的迁移路径:
复制代码v0.3.x Sender → SenderCompat(快速迁移,改导入即可)→ v0.4 Sender(按需完全升级)
| 变更类型 | v0.3.x | v0.4 | 说明 |
|---|---|---|---|
| 语音输入 | allow-speech prop | VoiceButton 独立组件 | 组件化设计,通过插槽集成 |
| 文件上传 | allow-files + button-group | UploadButton 独立组件 | 扁平化配置,更灵活 |
| 智能联想 | suggestions prop | Suggestion 扩展 | 通过 extensions 接入 |
| @提及 | - | Mention 扩展 | v0.4 新增功能 |
| 模板填充 | v-model:templateData | Template 扩展 | 响应式 ref 驱动 |
| 主题 | theme prop | ThemeProvider 包裹 | 全局继承,所有子组件自动生效 |
| 模板类型名 | type: 'template' | type: 'block' | 类型名称变更,需手动适配 |
如果你准备完全升级到 v0.4,下面是一个典型的迁移对照:
复制代码<!-- v0.3.x 写法 -->
<template>
<TrSender
v-model="content"
:allow-speech="true"
:speech="{ lang: 'zh-CN' }"
:allow-files="true"
:button-group="{ file: { accept: 'image/*' } }"
:suggestions="filteredSuggestions"
@speech-end="onSpeechEnd"
@files-selected="onFilesSelected"
/>
</template><!-- v0.4 写法 -->
<template>
<TrSender
v-model="content"
:extensions="extensions"
>
<template #footer-right>
<UploadButton accept="image/*" @select="onFilesSelected" />
<VoiceButton :speech-config="{ lang: 'zh-CN' }" @speech-final="onSpeechEnd" />
</template>
</TrSender>
</template><script setup lang="ts">
import { ref } from 'vue'
import { TrSender, UploadButton, VoiceButton } from '@opentiny/tiny-robot'const content = ref('')const extensions = [
TrSender.Suggestion.configure({ items: [], filterFn: myFilter }),
]const onFilesSelected = (files: File[]) => { /* ... */ }
const onSpeechEnd = (transcript: string) => { /* ... */ }
</script>
| 事件名 | 说明 | 回调参数 |
|---|---|---|
update:modelValue | 内容更新 | (value: string) |
submit | 提交内容 | (text: string, data?: StructuredData) |
cancel | 取消操作(loading 状态下点击停止) | () |
clear | 清空内容 | () |
focus | 获得焦点 | (event: FocusEvent) |
blur | 失去焦点 | (event: FocusEvent) |
input | 输入变化 | (value: string) |
| 方法 | 说明 | 参数 |
|---|---|---|
focus() | 使输入框获取焦点 | - |
blur() | 使输入框失去焦点 | - |
clear() | 清空输入内容 | - |
submit() | 手动触发提交 | - |
setContent(content) | 设置编辑器内容 | (content: string) |
getContent() | 获取编辑器内容 | - 返回 string |
cancel() | 手动触发取消 | - |
TinyRobot Sender 通过以下设计原则,为 AI 聊天应用提供了企业级的输入体验:
无论是构建一个简单的 AI 问答输入框,还是打造复杂的企业级 Copilot 交互界面,Sender 都能胜任。
OpenTiny NEXT 是一套企业智能前端开发解决方案,以生成式 UI 和 WebMCP 两大核心技术为基础,对现有传统的 TinyVue 组件库、TinyEngine 低代码引擎等产品进行智能化升级,构建出面向 Agent 应用的前端 NEXT-SDKs、AI Extension、TinyRobot智能助手、GenUI等新产品,加速企业应用的智能化改造,实现AI理解用户意图自主完成任务。
欢迎加入 OpenTiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
OpenTiny 官网:opentiny.design/ TinyRobot 代码仓库:github.com/opentiny/ti… (欢迎star ⭐) TinyRobot skill源码:github.com/opentiny/ag… (欢迎 Star ⭐)