前端AI输入框多行自适应与快捷键

4 阅读3分钟

结论摆前面:AI 对话输入框,别用 <input>,也别用固定高度的 <textarea>。要的是"打一行高一行、回车发送、Shift+回车换行"那种手感,跟主流 AI 产品对齐。我给一个 AI 助手做输入框,核心就这两件事:高度自适应 + 快捷键。看着简单,细节不少。

高度自适应:别用 scrollHeight 硬怼

很多教程教你监听 input、读 scrollHeightheight。能用,但有个老毛病:高度只增不减,删字时回不去。正确姿势是每次先把高度清零再读:

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,省了部署算力那一摊事。