构建交互式聊天界面-2
1. 前情回顾
基于之前的需求,我们的聊天中包含一些简单的交互功能,例如语音转文本:
- 用户点击语音识别按钮,前端进行语音转文字操作,此时页面展示“Please Talk, I am listening”的消息,消息的左侧包含动画效果
- 再次点击语音识别按钮,将转换后的文字展示出来,供用户确认
- 用户点击确认后,将发送确认的文本
- 用户点击取消后,取消所有的消息展示
我们将之前的这几篇结合起来实现该需求:
2. 功能实现
-
loading动画
使用 gka 生成雪碧图,建议将该效果封装为组件
-
安装
gka
工具:npm install gka -g
-
生成雪碧图:使用
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
用于合图优化 -
编写组件代码:在
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. 文字打字机效果
-
定义组件:在组件中,使用状态来存储当前显示的文本,并使用一个副作用来逐步更新这个状态,模拟打字机的打字效果。
-
实现打字机逻辑:在
useEffect
中,使用setInterval
来逐字显示文本,直到文本完成。 -
清理定时器:确保在组件卸载时清除定时器,避免内存泄漏。
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;