纯CSS实现AI Chat流式输出自动滚动到底部

569 阅读1分钟

现在的大模型聊天对话都有一个统一的交互:流式输出时,自动滚动到底部,如果用户手动滚动了就不再自动滚动到底部,这时如果手动滚到底部了也会再次继续触发自动滚动到底部。

那么是如何实现的呢?主流有两种方案:

  1. JavaScript 实现:监听 scroll 事件,要处理很多边界情况,挺麻烦的。
  2. CSS 实现:通过 flex-direction: column-reverse; 实现,简单快捷。

以下是用 CSS 实现的 demo

核心代码:

.chater {
  overflow: auto;
  display: flex;
  flex-direction: column-reverse;
}
.msger {
  flex: 1;
  display: flex;
  flex-direction: column;
}

完整代码:

<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>AI Chat 滚动</title>
  <style>
    * {
      box-sizing: border-box;
    }
    body {
      margin: 0;
      height: 100vh;
      display: flex;
      flex-direction: column;
    }
    .chater {
      flex: 1;
      overflow: auto;
      padding: 1em;
      display: flex;
      flex-direction: column-reverse;
    }
    .msger {
      flex: 1;
      display: flex;
      flex-direction: column;
    }
    .msger > pre {
      background-color: #eee;
      padding: 1em;
      border-radius: 8px;
      white-space: pre-wrap;
    }
    .msger .user {
      align-self: flex-end;
      margin-left: 10vw;
    }
    .msger .ai {
      align-self: flex-start;
      margin-right: 10vw;
    }
    .sender {
      flex-shrink: 0;
      padding: 1em;
    }
    textarea {
      width: 100%;
    }
  </style>
</head>

<body>
  <div class="chater">
    <div class="msger"></div>
  </div>
  <div class="sender">
    <textarea rows="6" placeholder="Enter发送,Shift+Enter换行" enterkeyhint="send"></textarea>
  </div>
  <script>
    const textarea = document.querySelector('textarea')
    const chater = document.querySelector('.chater')
    const msger = document.querySelector('.msger')

    textarea.addEventListener('keydown', function(e) {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault()
        send()
      }
    })

    function send() {
      chater.scrollTop = chater.scrollHeight

      const pre = document.createElement('pre')
      pre.className = 'user'
      pre.textContent = textarea.value
      textarea.value = ''
      msger.appendChild(pre)

      const pre2 = document.createElement('pre')
      pre2.className = 'ai'
      const timer = setInterval(() => {
        pre2.textContent +=
          String.fromCharCode(Math.floor(Math.random() * 122) + 65).repeat(
            Math.ceil(Math.random() * 10)
          ) + (Math.random() < 0.2 ? '\n' : ' ')
        if (pre2.textContent.length > 1000) clearInterval(timer)
      }, 50)
      msger.appendChild(pre2)
    }
  </script>
</body>

</html>

还有一个细节,输入框支持Enter发送,Shift+Enter换行

textarea.addEventListener('keydown', function(e) {
  if (e.key === 'Enter' && !e.shiftKey) {
    e.preventDefault()
    send()
  }
})

在 Vue 中实现更简洁:

<textarea
  placeholder="请输入,Enter发送,Shift+Enter换行"
  @keydown.enter.exact.prevent="send"
  @keydown.enter.shift="() => {}"
></textarea>

.enter.exact 表示仅按下 Enter