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;
  }
}
相关推荐
kyriewen6 小时前
Anthropic 估值逼近万亿美元,Claude Sonnet 5 + Claude Science 一天两连发
前端·ai编程·claude
小徐_23337 小时前
Wot UI 2.2.0 发布:Button 新增 subtle,VideoPreview 预览体验继续增强
前端·微信小程序·uni-app
山河木马9 小时前
矩阵专题3-怎么创建投影矩阵(uProjectionMatrix)
javascript·webgl·计算机图形学
天蓝色的鱼鱼9 小时前
关于 CSS 你可能不知道的属性,但关键时刻很有用
前端·css
泯泷10 小时前
第 2 篇:设计第一套字节码:Opcode、Instruction 与 Constant Pool
前端·javascript·安全
妙码生花10 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(十五):优化细节、网络请求封装
前端·后端·ai编程
泯泷10 小时前
第 1 篇:从 1 + 2 开始:亲手写出第一台 JSVM
前端·javascript·安全
团团崽_七分甜10 小时前
Spring Boot 核心知识点总结
前端
lichenyang45311 小时前
从一个按钮开始,理解 ASCF 框架到底在做什么
前端