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;
  }
}
相关推荐
zwjapple6 小时前
docker-compose一键部署全栈项目。springboot后端,react前端
前端·spring boot·docker
像风一样自由20208 小时前
HTML与JavaScript:构建动态交互式Web页面的基石
前端·javascript·html
伍哥的传说8 小时前
React 各颜色转换方法、颜色值换算工具HEX、RGB/RGBA、HSL/HSLA、HSV、CMYK
深度学习·神经网络·react.js
aiprtem9 小时前
基于Flutter的web登录设计
前端·flutter
浪裡遊9 小时前
React Hooks全面解析:从基础到高级的实用指南
开发语言·前端·javascript·react.js·node.js·ecmascript·php
why技术9 小时前
Stack Overflow,轰然倒下!
前端·人工智能·后端
GISer_Jing9 小时前
0704-0706上海,又聚上了
前端·新浪微博
止观止9 小时前
深入探索 pnpm:高效磁盘利用与灵活的包管理解决方案
前端·pnpm·前端工程化·包管理器
whale fall9 小时前
npm install安装的node_modules是什么
前端·npm·node.js
烛阴9 小时前
简单入门Python装饰器
前端·python