噜噜旅游App(4)——构建旅游智能客服模块,实现AI聊天

引言

模块效果图

在现代旅游应用中,提供一个智能客服模块可以极大地提升用户体验。通过集成先进的AI技术,我们可以为用户提供实时的旅行建议、解答疑问以及个性化推荐等服务。本文将详细介绍如何使用 React 和 react-vant 构建一个简洁而强大的旅游智能客服模块,并展示如何与后端聊天API进行交互以实现自然语言处理功能。

代码设计与实现聊天界面

1. 完整代码

Pages/Trip/index.jsx
jsx 复制代码
import {
  useEffect,
  useState,
} from 'react'
import useTitle from '@/hooks/useTitle'
import{
  chat
} from '@/llm'
import styles from './trip.module.css'
import{
  Input,
  Button,
  Loading,
  Toast,
} from 'react-vant'
import {
  ChatO,
  UserO,
} from '@react-vant/icons'
const Trip = () => {
  useTitle('旅游智能客服')
  const [text,setText] = useState('')
  const [isSending,setIsSending] = useState(false)
  const [messages,setMessages] = useState([
    {
      id:1,
      content:'hello~',
      role:'user'
    },
    {
      id:2,
      content:'hello,I am your assistant ,I can help you with your travel',
      role:'assistant'
    }
  ])

  const handleChat =async()=>{  
    if(text.trim() === '') {
    Toast.info({
      message:'内容不能为空'
    })
    return
  }
  setIsSending(true)
  setText('')

  setMessages((prev)=>{
    return [
    ...prev,
    {
      role:'user',
      content:text
    }
  ]
  })
  const newMessage = await chat([{
    role:'user',
    content:text
  }]);
  setMessages((prev)=>{
    return [
      ...prev,
      newMessage.data
    ]
  })
  setIsSending(false)
  }
  
  return (
    <div className='flex flex-col h-all'>
      <div className={`flex-1 ${styles.chatArea}`}>
        {
          messages.map((msg,index)=>{
            return (
              <div 
              key={index} {/*由于底部导航栏没有顺序变化,所以直接用索引作为key*/}
              className={
                msg.role === 'user' ? styles.messageRight : styles.messageLeft
              }
              >
                {
                  msg.role=='assistant'?
                  <ChatO/>:
                  <UserO/>
                }
                {msg.content}
              </div>
            )
          })
        }
      </div>
      <div className={`flex ${styles.inputArea}`}>
        <Input 
          value={text}
          onChange={(e)=>setText(e)}
          placeholder='请输入消息'
          className={`flex-1 ${styles.input}`}
        />
        <Button
          type='primary'
          onClick={handleChat}
          disabled={isSending}
        >
          发送
        </Button>
      </div>
       {isSending && (<div className='fixed-loading'>{/*离开文档流 */}
        <Loading type='ball'/>
      </div>)}

    </div>
  )
}
export default Trip
llm/index.js
js 复制代码
const DEEPSEEK_CHAT_API_URL = 'https://api.deepseek.com/chat/completions';

// 聊天Chat AI
export const chat = async (
    messages, 
    api_url = DEEPSEEK_CHAT_API_URL,
    api_key = import.meta.env.VITE_DEEPSEEK_API_KEY,
    model = 'deepseek-chat'
) => {
    try {
        const response = await fetch(api_url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${api_key}`
            },
            body: JSON.stringify({
                model,
                messages,
                stream: false,
            })
        })
        const data = await response.json();
        return {
            code: 0,
            data: {
                role: 'assistant',
                content: data.choices[0].message.content
            }
            
        }
    } catch(err) {
        return {
            code: 0,
            msg: '出错了...'
        }
   } 
}
Pages/Trip/trip.modules.css
css 复制代码
.chatArea {
    overflow-y: auto;
    padding: 12px;
    background-color: #f7f8fa;
}
.inputArea {
    padding: 16px;
    border-top: 1px solid #ddd;
    background: white;
}
.input {
    margin-right:8px;
}
.messageLeft, .messageRight{
    max-width: 70%;
    padding: 8px 12px;
    margin: 12px 0;
    border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.messageLeft {
    background-color: #bcd3ef;
}
.messageRight {
    background-color: #6eb16b;
    margin-left: 30%;
}

2. 界面布局

聊天界面由两大部分组成:消息显示区输入区。消息显示区负责展示用户和助手之间的对话记录;输入区则允许用户输入新的消息并发送给服务器。

jsx 复制代码
<div className='flex flex-col h-all'>
  <div className={`flex-1 ${styles.chatArea}`}>
    {/* 消息列表 */}
  </div>
  <div className={`flex ${styles.inputArea}`}>
    {/* 输入框和发送按钮 */}
  </div>
</div>

2.1 样式定制

为了使聊天界面更加美观,我们定义了一些基本的CSS样式。使用模块化css

css 复制代码
.chatArea {
  overflow-y: auto;
  padding: 12px;
  background-color: #f7f8fa;
}
.messageLeft, .messageRight{
  max-width: 70%;
  padding: 8px 12px;
  margin: 12px 0;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.messageLeft {
  background-color: #bcd3ef;
}
.messageRight {
  background-color: #6eb16b;
  margin-left: 30%;
}

3. 数据驱动与逻辑处理

3.1 状态管理

使用 useState 钩子来管理聊天记录的状态。每当有新消息时,更新消息数组并将新消息添加到列表中。

jsx 复制代码
 const [text,setText] = useState('') // 用户聊天框输入的信息
 const [isSending,setIsSending] = useState(false) // 是否已发送
 const [messages,setMessages] = useState([ // 聊天记录
     {
      id:1,
      content:'hello~',
      role:'user' // 用户
    },
    {
      id:2,
      content:'hello,I am your assistant ,I can help you with your travel',
      role:'assistant' // AI助手
    }
  ])

这里有两个角色:user用户和assistant助手,我们需要判断一下角色并决定将其的对话框展示在哪边(左右)。

jsx 复制代码
messages.map((msg,index)=>{
    return (
      <div 
      key={index}
      className={
        msg.role === 'user' ? styles.messageRight : styles.messageLeft
      }
      >
        {
          msg.role=='assistant'?
          <ChatO/>:
          <UserO/>
        }
        {msg.content}
      </div>
    )
})

在类名中使用三元运算符:msg.role === 'user' ? styles.messageRight : styles.messageLeft来决定显示在哪侧。并用 msg.role=='assistant'?<ChatO/>:<UserO/>显示对应的图标。

css 复制代码
// trip.module.css
.messageLeft, .messageRight{
    max-width: 70%;
    padding: 8px 12px;
    margin: 12px 0;
    border-radius: 8px;
    box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.messageLeft {
    background-color: #bcd3ef;
}
.messageRight {
    background-color: #6eb16b;
    margin-left: 30%;
}

3.2 发送消息

当用户点击"发送 "按钮时,调用 handleChat 函数处理消息发送逻辑。包括验证输入内容是否为空、设置发送状态、更新本地消息列表以及调用聊天API获取回复。

jsx 复制代码
const handleChat =async()=>{  
    if(text.trim() === '') {
    Toast.info({
      message:'内容不能为空'
    })
    return
  }
  setIsSending(true) // 已发送
  setText('') // 清空输入框,提升用户体验

  setMessages((prev)=>{ // 更新用户提问
    return [
    ...prev,
    {
      role:'user',
      content:text
    }
  ]
  })
  
  const newMessage = await chat([{ // 更新AI客服回答
    role:'user', // 切记传的是数组!!
    content:text
  }]);
  
  setMessages((prev)=>{
    return [
      ...prev,
      newMessage.data
    ]
  })
  setIsSending(false)// 改为未发送
 }
3.2.1 bug:闭包陷阱

在写项目的过程中,我一开始写的是这样的代码:

jsx 复制代码
const handleChat = async () => {
    if (text.trim() === "") {
        Toast.info({ message: '内容不能为空' });
        return;
    }
    setIsSending(true);
    setText('')
    // ❌ 闭包陷阱:捕获当前作用域的旧 messages 值
    setMessages([...messages, { role: 'user', content: text }]);

    const newMessage = await chat([{ role: 'user', content: text }]);
    // ❌ 再次使用旧的 messages 值,导致状态更新异常
    setMessages([...messages, newMessage.data]);
    setIsSending(false);
};

这样导致用户提问后,当助手回答出现后,用户的提问就消失不见了。

如图:

什么是"闭包陷阱"?

在 React 中,函数组件每次渲染都会创建一个新的函数实例 ,包括 handleChat。如果你在异步操作中使用了状态变量(如 messages),而没有使用 setState 的函数式更新方式,那么拿到的可能是一个旧的状态快照(snapshot)
这就是所谓的 闭包陷阱

我们来分析一下刚刚的代码:

  1. 第一次点击:messages = [msg1, msg2]
  2. 执行 setMessages([...messages, userMsg1]) → 新状态是 [msg1, msg2, userMsg1]
  3. 第二次点击时,此时函数作用域中的 messages 还是旧的 [msg1, msg2]
  4. 执行 setMessages([...messages, userMsg2]) →新状态是 [msg1, msg2, userMsg2]-> 还是基于旧的 messages 添加新消息userMsg1被覆盖!!

这就导致了状态更新的混乱,当渲染时,遇到msg.role === 'user' ? styles.messageRight : styles.messageLeft,会理所应当的只显示助手,而之前用户的消息记录没了。

正确做法:使用函数式更新

React 的 setState 支持传入一个函数,这个函数的参数是当前最新的状态值。这样可以避免闭包陷阱:

jsx 复制代码
 // ✅ 使用函数式更新,确保拿到最新的 messages
setMessages(prev => [...prev, { role: 'user', content: text }]);
const newMessage = await chat([{ role: 'user', content: text }]);
// ✅ 再次使用函数式更新
setMessages(prev => [...prev, newMessage.data]);

4. 集成聊天API

4.1 定义聊天函数

创建一个异步函数 chat 来封装与聊天API的交互过程。该函数接收消息对象作为参数,并返回助手的回复。

可以去deepseek 官方文档中查看各种模型和使用方法:api-docs.deepseek.com/zh-cn/

或者使用其他大模型如MoonShotplatform.moonshot.cn/docs/introd...

js 复制代码
const DEEPSEEK_CHAT_API_URL = 'https://api.deepseek.com/chat/completions';

// 聊天Chat AI
export const chat = async (
    messages, //一个数组,包含用户和助手之间的对话历史,每个元素代表一条消息
    api_url = DEEPSEEK_CHAT_API_URL,//API请求的URL
    api_key = import.meta.env.VITE_DEEPSEEK_API_KEY, // API访问所需的密钥,从环境变量`VITE_DEEPSEEK_API_KEY`中获取,.env.local中
    model = 'deepseek-chat' // 使用的语言模型名称
) => {
    try {
        const response = await fetch(api_url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${api_key}`
            },
            body: JSON.stringify({
                model,
                messages,
                stream: false,// 不开启流式输出
            })
        })
        const data = await response.json();
        return {
            code: 0,
            data: {
                role: 'assistant',
                content: data.choices[0].message.content
            }
            
        }
    } catch(err) {
        return {
            code: 0,
            msg: '出错了...'
        }
   } 
}
  1. 发起HTTP请求 :使用fetch API向指定的api_url发起POST请求,发送的信息包括所选的模型、对话历史以及是否开启流式传输等信息。
  2. 处理响应 :一旦收到服务器的响应,就将其解析为JSON格式,并从中提取出助手的回答内容(假设API返回的数据结构中包含choices[0].message.content字段作为回答)。
  3. 错误处理:如果请求过程中出现任何异常,都会捕获该异常并返回一个带有错误码和简单错误信息的对象。

返回值

  • 成功时:返回一个对象,其中包含状态码code: 0和助手的回答数据
  • 失败时:返回一个对象,包含状态码code: 1和错误信息msg: '出错了...'

结尾

通过本文,我们学习了如何利用 Reactreact-vant 快速搭建一个具备实际功能的旅游智能客服模块。从界面设计到数据处理再到与后端API的集成,每一步都展示了如何高效地构建用户友好的前端应用。

希望这篇文章能够帮助你更好地理解并实践类似的开发流程。未来,我们可以进一步探讨如何优化性能、增强安全性以及扩展更多高级功能。

相关推荐
paopaokaka_luck几秒前
基于SpringBoot+Uniapp的健身饮食小程序(协同过滤算法、地图组件)
前端·javascript·vue.js·spring boot·后端·小程序·uni-app
患得患失94934 分钟前
【前端】【vscode】【.vscode/settings.json】为单个项目配置自动格式化和开发环境
前端·vscode·json
飛_36 分钟前
解决VSCode无法加载Json架构问题
java·服务器·前端
YGY Webgis糕手之路3 小时前
OpenLayers 综合案例-轨迹回放
前端·经验分享·笔记·vue·web
90后的晨仔3 小时前
🚨XSS 攻击全解:什么是跨站脚本攻击?前端如何防御?
前端·vue.js
Ares-Wang3 小时前
JavaScript》》JS》 Var、Let、Const 大总结
开发语言·前端·javascript
90后的晨仔3 小时前
Vue 模板语法完全指南:从插值表达式到动态指令,彻底搞懂 Vue 模板语言
前端·vue.js
德育处主任4 小时前
p5.js 正方形square的基础用法
前端·数据可视化·canvas
烛阴4 小时前
Mix - Bilinear Interpolation
前端·webgl
90后的晨仔4 小时前
Vue 3 应用实例详解:从 createApp 到 mount,你真正掌握了吗?
前端·vue.js