构建交互式聊天界面-2

1. 前情回顾

基于之前的需求,我们的聊天中包含一些简单的交互功能,例如语音转文本:

  1. 用户点击语音识别按钮,前端进行语音转文字操作,此时页面展示“Please Talk, I am listening”的消息,消息的左侧包含动画效果
  2. 再次点击语音识别按钮,将转换后的文字展示出来,供用户确认
  3. 用户点击确认后,将发送确认的文本
  4. 用户点击取消后,取消所有的消息展示

我们将之前的这几篇结合起来实现该需求:

react-chat-element 实战小计

语音转文字

雪碧图

2. 功能实现

  • loading动画

使用 gka 生成雪碧图,建议将该效果封装为组件

  1. 安装 gka 工具

    npm install gka -g
  2. 生成雪碧图:使用 gka 处理你的动画帧图片,生成雪碧图和 CSS 文件。

    gka dir path/to/your/images -o path/to/output -u -m -s

    这里,path/to/your/images 是包含你所有帧图片的目录,path/to/output 是输出目录。-u 选项用于相同图片复用优化,-m 用于图片压缩,-s 用于合图优化

  3. 编写组件代码:在 LoadingSpinner.js 中,引入生成的雪碧图和 CSS 文件,并创建一个组件,该组件使用 div 来显示雪碧图,并通过修改 div 的样式来调整动画的大小

    import React from 'react';
    import spriteSheet from './path/to/output/sprites.png'; // 雪碧图路径
    import './path/to/output/gka.css'; // gka 生成的 CSS 文件
    
    const LoadingSpinner = ({ size = '100px' }) => {
      return (
        <div className="gka-base" style={{ width: size, height: size, backgroundImage: `url(${spriteSheet})` }}></div>
      );
    };
    
    export default LoadingSpinner;

    在这个组件中,size 是一个可选 props,允许动态设置组件的大小。

  • 语音录入时展示 "Listening Message"

import { useEffect, useState } from 'react';
import { MessageBox } from 'react-chat-elements';
import 'react-chat-elements/dist/main.css';
import AISVG from '@/assets/imgs/AI.svg';
import UserSVG from '@/assets/imgs/user.svg';
import LoadingSpinner from '../LoadingSpinner';

const MessageList = () => {
  const [messageList, setMessageList] = useState([]);
  const [maskVisible, setMaskVisible] = useState(false);
  const [loadingTimer, setLoadingTimer] = useState(null);
  const [textToConfirm, setTextToConfirm] = useState('');

  useEffect(() => {
    scrollToBottom();
  }, [messageList]);

  const scrollToBottom = () => {
    const msgElement = document.getElementById('message-list');
    const scrollTop = msgElement.scrollTop;
    const scrollHeight = msgElement.scrollHeight;
    const clientHeight = msgElement.clientHeight;
    msgElement.scrollTop = scrollHeight - clientHeight;
  };

  const handleOpenFile = async (e, item) => {
    // 文件点击逻辑
  };
    
  const addListeningMessgae = () => {
    const newMessageList = messageList?.filter((item) => item.key !== 'speech2text');
    const newMessage = {
      key: 'speech2text',
      type: 'text',
      content: (
        <div className="listening-item">
          <div className="listening-item-text">
            <LoadingSpinner size="30px" />
            {'Please talk, I am listening...'}
          </div>
        </div>
      ),
      data: {
        listening: true
      }
    };
    newMessageList.push(newMessage);
    setMessageList(newMessageList);
  };

  return (
    <>
      <div className="message-list" id="message-list" onScroll={() => scrollToBottom()}>
            <MessageBox
              avatar={AISVG}
              position="left"
              type="text"
              text="Hello, I am Univers AI. I am the first professional AI in the sustainable industry, dedicated to helping you optimize your building systems."
              className="message-item message-item-left"
            />
            <div className="init-list">
              {!messageList?.length &&
                initContents.map((item, index) => (
                  <InitComponent title={item?.title} content={item?.content} key={item?.key} index={index} tabKey={item?.key} />
                ))}
            </div>
            {messageList.map((item) => (
              <MessageBox
                key={item?.key}
                avatar={item?.isBot ? UserSVG : AISVG}
                position={item?.isBot ? 'right' : 'left'}
                type={item?.type}
                text={item?.type === 'text' ? item?.content : item?.text}
                data={item?.data}
                onOpen={item?.type === 'photo' || item?.type === 'file' ? (e) => handleOpenFile(e, item) : null}
                onDownload={item?.type === 'photo' || item?.type === 'file' ? (e) => handleOpenFile(e, item) : null}
              />
            ))}
        )}
      </div>
    </>
  );
};
export default MessageList;
  • 语音录入停止后展示"Text Confirm Message"

const addConfirmMessage = () => {
    const newMessageList = messageList?.filter((item) => item.key !== 'speech2text');
    const newMessage = {
      key: 'speech2text',
      type: 'text',
      content: (
        <div className="listening-item">
          <div className="listening-item-text">
            <LoadingSpinner size="30px" />
            {textToConfirm || 'Please talk, I am listening...'}
          </div>
          {!speaking && renderOptions()}
        </div>
      ),
      data: {
        listening: true
      }
    };
    newMessageList.push(newMessage);
    setMessageList(newMessageList);
  };

const renderOptions = () => {
    return (
      <div className="listening-item-options">
        <div className="listening-item-options-item reject" onClick={rejectText}>
          ×
        </div>
        <div className="listening-item-options-item confirm" onClick={confirmText}>
          √
        </div>
      </div>
    );
  };

  const rejectText = () => {
    // 停止语音识别
    recognizer?.stopContinuousRecognitionAsync();
    const newMessageList = messageList.filter((item) => item.key !== 'speech2text');
    setMessageList(newMessageList);
    setTextToConfirm('');
  };

  const confirmText = () => {
     // 停止语音识别
    recognizer?.stopContinuousRecognitionAsync();
    const newMessageList = [...messageList];
    newMessageList[newMessageList.length - 1] = {
      key: uuid(),
      type: 'text',
      content: textToConfirm
    };
    setMessageList(newMessageList);
    setTextToConfirm('');
  };

效果展示

3. 文字打字机效果

  1. 定义组件:在组件中,使用状态来存储当前显示的文本,并使用一个副作用来逐步更新这个状态,模拟打字机的打字效果。

  2. 实现打字机逻辑:在 useEffect 中,使用 setInterval 来逐字显示文本,直到文本完成。

  3. 清理定时器:确保在组件卸载时清除定时器,避免内存泄漏。

import React, { useState, useEffect } from 'react';

function Typewriter({ text, typingSpeed }) {
  const [displayText, setDisplayText] = useState('');
  const [currentIndex, setCurrentIndex] = useState(0);

  useEffect(() => {
    const interval = setInterval(() => {
      if (currentIndex < text.length) {
        setDisplayText((prevText) => prevText + text[currentIndex]);
        setCurrentIndex((prevIndex) => prevIndex + 1);
      } else {
        clearInterval(interval);
      }
    }, typingSpeed);
    return () => {
      clearInterval(interval);
    };
  }, [text, typingSpeed, currentIndex]);

  return <div>{displayText}</div>;
}

export default Typewriter;

 

posted @ 2024-12-05 22:17  Yang9710  阅读(20)  评论(0编辑  收藏  举报