AI语音助手 React 组件使用js-audio-recorder实现,将获取到的语音转成base64发送给后端,后端接口返回文本内容

页面效果:

js代码:

javascript 复制代码
import React, { useState, useRef, useEffect } from 'react';
import { Layout, List, Input, Button, Avatar, Space, Typography, message } from 'antd';
import { SendOutlined, UserOutlined, RobotOutlined, AudioOutlined, StopOutlined } from '@ant-design/icons';
import JsAudioRecorder from 'js-audio-recorder';
import './style.less';

const { Header, Content, Footer } = Layout;
const { Text } = Typography;

const ChatInterface = () => {
  const [messages, setMessages] = useState([
    { sender: 'assistant', content: '你好!我是AI助手,有什么可以帮你的吗?' },
  ]);
  const [inputValue, setInputValue] = useState('');
  const [isRecording, setIsRecording] = useState(false);
  const messagesEndRef = useRef(null);
  const recorderRef = useRef(null);

  // 初始化录音器
  useEffect(() => {
    recorderRef.current = new JsAudioRecorder({
      sampleBits: 16,
      sampleRate: 16000,
      numChannels: 1,
    });
    return () => {
      if (recorderRef.current) {
        recorderRef.current.destroy();
      }
    };
  }, []);

  // 开始/停止录音
  const toggleRecording = () => {
    if (isRecording) {
      stopRecording();
    } else {
      startRecording();
    }
  };

  // 开始录音
  const startRecording = () => {
    recorderRef.current.start().then(() => {
      setIsRecording(true);
      message.success('录音中...');
    }).catch((err) => {
      message.error('录音失败: ' + err.message);
    });
  };

  // 停止录音并发送
  const stopRecording = () => {
    try {
      recorderRef.current.stop();
      setIsRecording(false);
      message.success('录音结束,处理中...');

      const blob = recorderRef.current.getWAVBlob();
      console.log(blob);

      const reader = new FileReader();
      reader.onloadend = () => {
        const base64Data = reader.result.split(',')[1];
        console.log(base64Data);
        sendAudioToAPI(base64Data);
      };

      reader.onerror = () => {
        message.error('音频转换失败');
      };
      reader.readAsDataURL(blob);
    } catch (err) {
      message.error('停止录音失败: ' + err.message);
    }
  };

  // 模拟API调用
  const sendAudioToAPI = (base64Data) => {
    setTimeout(() => {
      const mockResponse = { text: '这是语音识别后的文本(模拟数据)' };
      setInputValue(mockResponse.text);
      message.success('语音识别完成!');
    }, 1500);
  };

  const handleSend = () => {
    if (!inputValue || inputValue.trim() === '') {
      message.warning('消息不能为空!');
      return;
    }

    // 添加用户消息
    setMessages([...messages, { sender: 'user', content: inputValue }]);
    setInputValue('');

    // 模拟AI回复
    setTimeout(() => {
      setMessages(prev => [...prev, { sender: 'assistant', content: `这是对你"${inputValue}"的回复。` }]);
    }, 1000);
  };

  // 自动滚动到底部
  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  return (
    <Layout className="chat-layout">
      <Header className="chat-header">
        <Text strong style={{ color: 'white', fontSize: '18px' }}>
          AI聊天助手(支持语音输入)
        </Text>
      </Header>
      <Content className="chat-content">
        <div className="message-container">
          {messages.map((item, index) => (
            <div
              key={index}
              className={`message-wrapper ${item.sender}`}
            >
              <div className={`message-bubble ${item.sender}`}>
                <div className="message-avatar">
                  <Avatar
                    icon={item.sender === 'user' ? <UserOutlined /> : <RobotOutlined />}
                    style={{ backgroundColor: item.sender === 'user' ? '#1890ff' : '#52c41a' }}
                  />
                </div>
                <div className="message-content">
                  <div className="message-sender">
                    {item.sender === 'user' ? '你' : 'AI助手'}
                  </div>
                  <div className="message-text">
                    {item.content}
                  </div>
                </div>
              </div>
            </div>
          ))}
          <div ref={messagesEndRef} />
        </div>
      </Content>
      <Footer className="chat-footer">
        <Space.Compact style={{ width: '100%' }}>
          <Button
            type={isRecording ? 'danger' : 'default'}
            icon={isRecording ? <StopOutlined /> : <AudioOutlined />}
            onClick={toggleRecording}
          />
          <Input
            placeholder={isRecording ? '正在录音...' : '输入消息或语音...'}
            value={inputValue}
            onChange={(e) => setInputValue(e.target.value)}
            onPressEnter={handleSend}
            disabled={isRecording}
          />
          <Button
            type="primary"
            icon={<SendOutlined />}
            onClick={handleSend}
            disabled={isRecording}
          >
            发送
          </Button>
        </Space.Compact>
      </Footer>
    </Layout>
  );
};

export default ChatInterface;

less代码:

css 复制代码
/* 整体布局 */
.chat-layout {
  height: 100vh;
  display: flex;
  flex-direction: column;
  background-color: #f5f5f5;
}

.chat-header {
  background-color: #1e88e5;
  padding: 0 24px;
  display: flex;
  align-items: center;
  height: 64px;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}

.chat-content {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
  background-color: #eaeaea;
}

.chat-footer {
  padding: 12px 16px;
  background: #f0f2f5;
  border-top: 1px solid #e8e8e8;
}

/* 消息容器 */
.message-container {
  display: flex;
  flex-direction: column;
  gap: 12px;
}

/* 消息包装器 */
.message-wrapper {
  display: flex;
}

.message-wrapper.user {
  justify-content: flex-end;
}

.message-wrapper.assistant {
  justify-content: flex-start;
}

/* 消息气泡 */
.message-bubble {
  display: flex;
  max-width: 80%;
  gap: 8px;
}

.message-bubble.user {
  flex-direction: row-reverse;
}

/* 消息内容 */
.message-content {
  display: flex;
  flex-direction: column;
  max-width: calc(100% - 40px);
}

.message-sender {
  font-size: 12px;
  color: #666;
  margin-bottom: 4px;
}

.message-text {
  padding: 10px 14px;
  border-radius: 18px;
  line-height: 1.5;
  word-break: break-word;
}

/* 用户消息样式 */
.message-wrapper.user .message-text {
  background-color: #1890ff;
  color: white;
  border-top-right-radius: 4px;
}

/* AI消息样式 */
.message-wrapper.assistant .message-text {
  background-color: white;
  color: #333;
  border-top-left-radius: 4px;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}

/* 第一条欢迎消息全宽 */
.message-wrapper.assistant:first-child .message-bubble {
  max-width: 100%;
}

.message-wrapper.assistant:first-child .message-text {
  background-color: #f6ffed;
  border-radius: 8px;
  padding: 12px 16px;
}

/* 头像样式 */
.message-avatar {
  display: flex;
  align-items: flex-end;
  padding-bottom: 24px;
}

/* 移动端适配 */
@media (max-width: 768px) {
  .message-bubble {
    max-width: 90%;
  }
  
  .chat-footer {
    padding: 8px;
  }
}
相关推荐
寧笙(Lycode)3 分钟前
React系列——HOC高阶组件的封装与使用
前端·react.js·前端框架
asqq86 分钟前
CSS 中的 ::before 和 ::after 伪元素
前端·css
拖孩28 分钟前
【Nova UI】十五、打造组件库之滚动条组件(上):滚动条组件的起步与进阶
前端·javascript·css·vue.js·ui组件库
苹果电脑的鑫鑫37 分钟前
element中表格文字剧中可以使用的属性
javascript·vue.js·elementui
Hejjon41 分钟前
Vue2 elementUI 二次封装命令式表单弹框组件
前端·vue.js
一丝晨光1 小时前
数值溢出保护?数值溢出应该是多少?Swift如何让整数计算溢出不抛出异常?类型最大值和最小值?
java·javascript·c++·rust·go·c·swift
小堃学编程1 小时前
前端学习(3)—— CSS实现热搜榜
前端·学习
Wannaer2 小时前
从 Vue3 回望 Vue2:响应式的内核革命
前端·javascript·vue.js
不灭锦鲤2 小时前
xss-labs靶场基础8-10关(记录学习)
前端·学习·xss
Bl_a_ck2 小时前
--openssl-legacy-provider is not allowed in NODE_OPTIONS 报错的处理方式
开发语言·前端·web安全·网络安全·前端框架·ssl