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;
  }
}
相关推荐
BillKu1 小时前
Vue3 + TypeScript中provide和inject的用法示例
javascript·vue.js·typescript
培根芝士1 小时前
Electron打包支持多语言
前端·javascript·electron
Baoing_2 小时前
Next.js项目生成sitemap.xml站点地图
xml·开发语言·javascript
mr_cmx2 小时前
Nodejs数据库单一连接模式和连接池模式的概述及写法
前端·数据库·node.js
东部欧安时2 小时前
研一自救指南 - 07. CSS面向面试学习
前端·css
沉默是金~2 小时前
Vue+Notification 自定义消息通知组件 支持数据分页 实时更新
javascript·vue.js·elementui
涵信2 小时前
第十二节:原理深挖-React Fiber架构核心思想
前端·react.js·架构
在下千玦2 小时前
#去除知乎中“盐选”付费故事
javascript
ohMyGod_1232 小时前
React-useRef
前端·javascript·react.js