创新实训-前端聊天页面

概述

聊天页面旨在依照chatgpt完成一个类似的聊天页面,主要分为三部分:输入框,聊天容器和聊天对话管理侧边栏,使用到的组件技术主要有vue,vuex,tailwind

类定义

这里使用ts,可以定义各个类,这可以很好的理清思路,同时可以知道这个类用在哪几个组件中。

message

messageType定义了发给后端的信息,包含的信息有

  1. idx用于定义一条message在一个conversation中是第几条
  2. role表示的是说话角色,这里我们按照智谱清言的定义方式,其中assistant为ai的回答,user为用户的问题,system为系统设定(system在实际中不常使用)
  3. speech为回答数组,用于ai重新生成回答时使用
  4. timestamp为消息发送时间(待完成
  5. suitable为是否赞同回答(这个是按照openai的策略,实现用户重新回答后评分,暂未完成)
export interface messageType {
    idx: number;
    loading: boolean;
    role: string;
    speeches: string[];
    timestamp?: number;
    suitable: number[];
 }

convType

conversation就是在多个message的集合加上几个属性进行扩展,

  1. id这里的id表示的是当前有几个会话
  2. timestamp表示会话创建时间
  3. msgList就是messageType的数组
  4. selected表示该会话是否被选择,可以用于样式的更新
export interface convType {
    id: number;
    title: string;
    timestamp?: number;
    msgList?: messageType[];
    selected?:boolean;
}

convListType

convList表示多个conversation的集合,这里直接使用conversation组成的数组表示,就不详细写了

代码概述

该界面主要分为三部分,分别是侧边栏,消息显示区和输入框,三部分分别负责不同的内容,其中侧边栏主要负责会话管理,包括会话的增删改;消息显示区主要负责对话和页面的渲染;输入框我在项目中用于与后端的对话和信息传递。

组件之间的通讯我选择使用vuex进行全局变量管理,管理变量如下

conversation: {
    id: 0,
    title: "",
    msgList: [],
},
chatStatus: "available",
chatMsg: "",
conversationList: [],
selectedIdx: -1

在项目中,我使用了eventbus使用发布订阅的模式实现了兄弟组件的通信,用于在一个vue组件中调用另一个vue的方法。

下面我就我完成的这三部分做一个简单的介绍,包括代码设计等。

输入框

功能

简单介绍一下输入框的主要功能吧,主要功能一共有三个

  1. 信息输入,用户输入信息,输入框要随着输入信息而扩大
  2. 问题发送,这个功能比较麻烦,需要和后端和其他模块进行交互
  3. 重新回答:点击之后重新生成回答(待完成)

页面

需要实现的静态页面很简单,一个输入文本的输入框,一个发送和停止可以切换的按钮和一个重新生成对话的按钮,这里就不给静态代码了,太长了。

内部功能

输入框自适应

一开始我直接使用最简单的输入框textarea,结果发现如果输入的字符串比输入框长的话之前的就会被遮住,非常丑,就模仿chatgpt实现了个输入框的自适应。

这里实现逻辑如下:

首先将chatMsg和textarea使用v-model进行双向绑定,对于chatMsg进行监听,在chatMsg变化的时候调用方法改变文本框高度

接着实现修改高度的方法,实现的changeHeight方法如下

    changeHeight() {
      var elem = this.$refs.inputChat;
      elem.style.height = '24px';
      var scrollHeight = elem.scrollHeight;
      if (24 >= scrollHeight || this.chatMsg.length == 0) {
        this.resetHeight();
        return;
      }

      elem.style.removeProperty("overflow-y")
      elem.style.height = scrollHeight + 'px';
      console.log(scrollHeight)

    },

这里使用ref通过引用来修改textarea的样式,scollHeight为元素的真实高度,如果高度≤24表示不用改,否则调用resetHeight方法修改高度

信息发送

发送消息需要实现一个和后端交互的接口,后端接口如下

@RequestMapping("/chat")
public class ChatAiController {

    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    @GetMapping(value = "/stream", produces = "text/event-stream")
    public SseEmitter chatStream(@RequestParam String modelName, @RequestParam String content) {
        StreamResponseHandler<AiMessage> handler = new StreamResponseHandler<>();
        StreamingChatLanguageModel aimodel= StreamingChatModelEnums.findValue(modelName);
        if (aimodel == null) {
            throw new IllegalArgumentException("Model not found");
        }
        aimodel.generate(content, handler);
        handler.get();
        return handler.getEmitter();
    }
}

信息初始化

时间:触发send方法

实现功能:

  • [x] 在store中插入两条初始化的信息。

实现流程:生成两条信息,分别为用户提问和ai回答,用户提问的信息内容为chatMsg即输入框中的内容

    addInitMessage(){
      this.addMessage({
        "idx": 0,
        "role": "user",
        "message": this.chatMsg
      })
      var message = {
        "idx": 0,
        "loading": true,
        "role": "assistant",
        "suitable": [0],
        "speeches": [""]
      }
      this.addMessage(message)
    },

后端请求流数据

时间:点击send触发后

实现功能:

  • [x] 调用后端流聊天接口
  • [x] 调用siderbar模块的newChat方法
  • [x] 修改conversation的message
    sseBuild(){
      var that = this;
      var source = this.source = new EventSource(`http://localhost:8080/chat/stream?content=${this.chatMsg}&&model=GLM3`);
      var message = this.addMessageToVConv()
      source.addEventListener("open", function () {
        console.log("connect");
        that.chatMsg = "";
        console.log("bus-send",that.conversation)

        bus.emit('bus-send',that.conversation.id)

      });

      //如果服务器响应报文中没有指明事件,默认触发message事件
      source.addEventListener("message", function (e) {

        message = that.conversation.msgList[that.conversation.msgList?.length - 1];
        //console.log("conversation",that.conversation)
        var content = e.data;
        console.log(`resp:(${e.data})`);
        // 滚动到最下面
        that.handleScrollBottom();
        message["speeches"][0] += content

        that.refreshConversation();
      });
      source.addEventListener('complete', (event) => {
        source.close();
        message["loading"] = false;
        that.convLoading = false;
        console.log(message)
        that.refreshConversation();
        that.refreshMessage(message)
        //that.refreshListByConv()
        that.source = undefined;
      });
      //发生错误,则会触发error事件
      source.addEventListener("error", function (e) {
        console.log("error:" + e.data);
        source.close();
        that.source = undefined;
      });
    },

发送

时间:点击send调用

实现

  • [x] 修改loading状态
  • [x] 加入初始化信息
  • [x] 滚动到最下面
  • [x] 调用chat或者streamchat
    send() {

      if (this.chatMsg.trim().length == 0) {
        return;
      }

      if (this.convLoading) {
        return;
      }

      this.convLoading = true;
      this.chatMsg = this.chatMsg.trim().replace(/\\n/g, "")
      this.addInitMessage()
      // 滚动到最下面
      this.handleScrollBottom();
      //this.streamChat();
      this.chat();

      //this.convLoading = false;

    },

侧边栏

侧边栏需要实现会话组的管理和显示,具体来说需要实现的功能有显示会话,新建会话,添加会话,删除会话,修改会话标题等。

修改标题

修改标题这个功能实现需要首先添加一个修改的图标,在点击修改图标之后会触发editTitle方法,将该会话的editable的值改为true,同时设置convTitletmp变量的值为当前的会话title,而当editable为true的时候,会触发v-if的渲染,将原来的text改成一个输入框,内容就是convTitiletmp,editTitle方法如下。

    editTitle(idx) {
      if (idx < 0 || idx >= this.conversations.length) {
        alert("invalid index")
        console.log("invalid index")
        return;
      }
      var conv = this.conversations[idx];
      this.convTitletmp = conv.title;
      conv.editable = true;
      this.conversations[idx] = conv;
      setTimeout(() => {
        document.getElementById("titleInput").focus();
      }, 150)
    },

取消修改

下面再说下取消修改的cancelChange方法,

触发方式:有两种,一种是在输入的时候丢失 了焦点调用,另一种是自己取消修改

    cancelChangeConvTitle(idx, conv) {
      conv.editable = false;
      this.conversations[idx] = conv;
      },

确认修改

输入:会话在列表中的位置idx,会话对象

实现功能:

  • [x] vuex中conversation修改
  • [x] vuex中conversationList修改
  • [x] localstorage中conversationList修改

具体实现:修改会话对象的标题,调用方法修改vuex中全局的conversationList对象,调用saveConversations方法,把conversations保存到loacalStorage中。

触发方式:在编辑完成新的会话标题之后点击确认调用

    changeConvTitle(idx, conv) {
      conv.title = this.convTitletmp;
      this.setConversationByIndex({index:idx,newConv:conv});
      this.saveConversations();
      this.cancelChangeConvTitle(idx, conv)
    },

会话删除


        delConversationByIndex(state:stateType, index:number) {
if (state.conversationList != null&&state.conversationList.length>index)
state.conversationList.splice(index, 1);
else console.log("delConversationByIndex fail",state.conversationList,index);
},

会话选择

传参:

实现功能

  • [x] 选中后高亮
  • [x] 页面中显示当前会话内容

实现简述:

首先判断是否可以可以选中cidx是否合法,之后添加属性表示选中并判断是否真实切换会话(可能是当前会话),旧会话切换选中属性,最后将vuex中的会话更新为当前会话

    selectConversation(cidx) {
      var that = this;
      if (cidx < 0 || cidx >= this.getConversationList.length) {
        console.log("invalid index")
        return;
      }
      //get the conversation which you want to select by index
      var conv = this.getConversationByIndex(cidx);
      console.log("selectConversation", cidx, conv)
      //if the conversation is already selected, return
      if (this.oldConv && this.oldConv.id == conv.id) {
        console.log("same conversation")
        return;
      }
      //set the old conversation to unselected
      if (this.oldConv) {
        this.oldConv.selected = false;
      }
      //set the new conversation to selected
      conv.selected = true
      this.setSelectedIdx(cidx);
      this.oldConv = conv;

      document.title = conv.title || "chatai";
      this.chatTitle = conv.title || "chatai";

      //set the conversation to vuex
      this.setConversation(conv);
      this.conversations[cidx] = conv;
    },

提供服务

新建chat

调用情形:当inputbox中触发send方法,且当前并未有会话时。

实现原理:通过bus总线进行兄弟组件之间的消息传递

实现流程:通过bus注册bus-send方法,当该方法被触发的时候调用busSend方法,表示inputBox模块触发了send方法。busSend方法先获取当前vuex中的conversation的id,再在conversationList中找这个会话,如果没找到就会新建一个会话,同时新会话的messageList为当前的messageList

报错

bug

焦点丢失

对于conversation 的title修改中,我一开始设置焦点丢失后立即调用canceledit方法,结束修改,但是在点击确定修改按钮后,也会先触发焦点丢失的取消修改方法,导致变量没被修改,修改失败

解决方法就是设置一个延时,让在失去焦点之后,不会立即调用取消修改的方法,而是在半秒钟之后取消。

传参

又是一个痛苦的bug调试了好久,才发现是store中的全局变量mutation和action都是只能传一个参数的,解决方法就是使用对象传递多个参数。。。累死了。。还是vue不太熟练

  • 9
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值