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;
  }
}
相关推荐
hh随便起个名3 小时前
力扣二叉树的三种遍历
javascript·数据结构·算法·leetcode
我是小路路呀4 小时前
element级联选择器:已选中一个二级节点,随后又点击了一个一级节点(仅浏览,未确认选择),此时下拉框失去焦点并关闭
javascript·vue.js·elementui
程序员爱钓鱼4 小时前
Node.js 编程实战:文件读写操作
前端·后端·node.js
PineappleCoder4 小时前
工程化必备!SVG 雪碧图的最佳实践:ID 引用 + 缓存友好,无需手动算坐标
前端·性能优化
JIngJaneIL5 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
敲敲了个代码5 小时前
隐式类型转换:哈基米 == 猫 ? true :false
开发语言·前端·javascript·学习·面试·web
澄江静如练_5 小时前
列表渲染(v-for)
前端·javascript·vue.js
JustHappy6 小时前
「chrome extensions🛠️」我写了一个超级简单的浏览器插件Vue开发模板
前端·javascript·github
Loo国昌6 小时前
Vue 3 前端工程化:架构、核心原理与生产实践
前端·vue.js·架构