结论摆前面:AI 对话输入框,别用 <input>,也别用固定高度的 <textarea>。要的是"打一行高一行、回车发送、Shift+回车换行"那种手感,跟主流 AI 产品对齐。我给一个 AI 助手做输入框,核心就这两件事:高度自适应 + 快捷键。看着简单,细节不少。
高度自适应:别用 scrollHeight 硬怼
很多教程教你监听 input、读 scrollHeight 设 height。能用,但有个老毛病:高度只增不减,删字时回不去。正确姿势是每次先把高度清零再读:
function AutoTextarea({ value, onChange, onSend }) {
const ref = useRef();
const resize = () => {
const el = ref.current;
el.style.height = 'auto'; // 关键:先归零
el.style.height = Math.min(el.scrollHeight, 160) + 'px';
};
useEffect(resize, [value]);
return (
<textarea ref={ref} value={value} rows={1}
onChange={(e) => onChange(e.target.value)}
onKeyDown={(e) => handleKey(e, onSend)}
placeholder="问点什么…(Shift+Enter 换行)" />
);
}
height = 'auto' 那一步不能省。Math.min(..., 160) 是封顶,超过约 6 行就内部滚动,不然贴一大段文档输入框能撑满半屏。160px 这个上限我调过,太矮显不全,太高挤掉对话区。
快捷键:Enter 发送,Shift+Enter 换行
这是 AI 输入框的标配,但中文输入法下有个大坑——输入法组词时按 Enter 是在选词,不该触发发送:
function handleKey(e, onSend) {
// 输入法组合中,isComposing 为 true,直接放行
if (e.nativeEvent.isComposing) return;
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
onSend();
}
// Shift+Enter 不拦,浏览器默认就换行
}
isComposing 这个判断是血泪经验。没加之前,中文用户打字打到一半,选词按回车,半句话直接发出去了,投诉一片。e.nativeEvent.isComposing 在组词期间为 true,这时啥都不做。
补充一个更稳的写法,有些旧浏览器 isComposing 不准,可以配合 compositionstart/end 自己记状态:
const composing = useRef(false);
// onCompositionStart={() => composing.current = true}
// onCompositionEnd={() => composing.current = false}
发送态:空内容不让发,发送中禁用
细节决定手感。空白(或纯空格)不该能发,发送中按钮要禁用防止连点:
function SendBtn({ value, sending, onSend }) {
const empty = !value.trim();
return (
<button className="send" disabled={empty || sending} onClick={onSend}>
{sending ? '…' : '发送'}
</button>
);
}
.send { padding: 6px 16px; border: none; border-radius: 8px;
background: #4080ff; color: #fff; cursor: pointer; transition: opacity .2s; }
.send:disabled { background: #c9cdd4; cursor: not-allowed; }
textarea { width: 100%; resize: none; border: none; outline: none;
font: inherit; line-height: 1.6; max-height: 160px; overflow-y: auto;
background: transparent; }
resize: none 关掉手动拖拽,自适应已经接管高度,留着那个拖拽角很碍眼。
一个我没做完美的地方
发送后我会清空输入框并把高度复位。但如果用户发送时输入法还在组词态,清空和组词会打架,偶尔残留半个字。我现在的处理是发送前先 el.blur() 强制结束组词再清,有点 hack,但比残字强。算个待优化项。
function send() {
ref.current.blur(); // 强制结束 IME 组词
doSend(value);
onChange('');
requestAnimationFrame(() => ref.current.focus());
}
requestAnimationFrame 里重新 focus,体验上几乎无感,光标还在输入框。
输入框这层是纯前端,后端对话能力我直接用了个零代码拖拽搭智能体的平台配好的接口,没自己写服务端。模型 API 调的讯飞 MaaS,省了部署算力那一摊事。