【极简】Vue写的chatGpt前端应用

苦于网上的Gpt都太贵了,申请了个自己的接口,刚好最近在学Vue所以写了这个前端页面,实现了连续对话功能,只写了个简单的页面,可在此基础上二开,布局有点乱,可以自己改改。
效果图:
在这里插入图片描述

1.对话记忆功能的实现
想法:把用户输入记录做一个存储,然后每次有新问题后连同新问题一起发送,为了节省费用,考虑使用切片数组。

如果有其他的问题,最多给一句提示:

  • 以上是我之前问你的问题,只供你回答我接下来问题做参考,请你不要重复回答我之前问过的问题,只对新问题进行处理。新问题是

其他的都通通交给聪明的gpt去处理,哈哈

代码:

		//在发送消息时将整个对话历史发送
        const historyString = JSON.stringify(this.chatHistory);
        // 提取用户说的消息并拼接成字符串
        // 假设 chatHistory 是包含完整对话历史的数组
        const maxHistoryLength = 4;  // 指定要保留的最大条目数
        // 裁剪对话历史到最近的 3-4 条信息
        const trimmedChatHistory = this.chatHistory.slice(-maxHistoryLength);
        console.log("裁剪的信息"+trimmedChatHistory)
      // 现在 trimmedChatHistory 中包含了最近的 3-4 条对话历史记录
        const userMessages = trimmedChatHistory
            .filter(item => item.sender === 'user') // 过滤出 sender 为 'user' 的对话记录
            .map(item => item.message); // 通过 map 方法获取每条消息的内容
        // 处理后台返回的响应消息
        // 合并消息为字符串
        const userMessageString = userMessages.join(' '); // 使用 join 方法将消息拼接成一个字符串,用空格分隔每条消息
        // 在拼接的消息后面加上新的问题
        const fullUserMessage = userMessageString + ' 以上是我之前问你的问题,只供你回答我接下来问题做参考,请你不要重复回答我之前问过的问题,只对新问题进行处理。新问题是:' + this.userInput;
        const response = await this.chatGPTRequest(fullUserMessage);
        this.handleBackendResponse(response);

2.防止不小心用Gpt4调用,做了个Gpt选项,加了个密码验证,可以写到后端。。。

 submitPassword() {
      if (this.password === 'useit') { // 替换为真实的密码
        ElNotification({
          title: 'Success',
          message: '权限已经获得!',
          type: 'success',
        })
        this.selectedModel = 'gpt-4'
        this.accessGranted = true; // 设置权限状态为已获得
        this.showPasswordCard = false;
        // this.password = ''; // 清空密码字段
      } else {
        ElNotification({
          title: 'Warning',
          message: '都是贫困惹的祸!',
          type: 'warning',
        })
        // this.selectedModel = 'gpt-4'
        this.selectedModel = 'gpt-3.5-turbo'
        // 密码错误处理
      }
    }
//问题提交验证
  if (this.selectedModel === 'gpt-4' && !this.accessGranted) {
        // 弹出提示或其他处理,要求用户先验证密码
        ElNotification({
          title: 'Warning',
          message: '都是贫困惹的祸!!',
          type: 'success',
        })
        return;
      }

3.模拟打字效果–后来才发现完全没必要,当使用流式输出的时候,貌似就可以直接是打字效果了。。。难得弄了。
4.MarkdownIt

//首先要npm install这个组件
// 假设你有一个元素用于显示 Markdown 渲染后的内容
const markdownContainer = document.getElementById('markdown-container');
const markdownText = '# Hello, Markdown!';
const html = marked(markdownText);
markdownContainer.innerHTML = html;
//在这个例子中,marked(markdownText) 函数会将 Markdown 文本转换为对应的 HTML,然后我们将这个 HTML 插入到页面中指定的元素中。

5.几个存储记录到本地的方法,配合记忆功能使用

  // 处理后台返回的响应消息
    handleBackendResponse(response) {
      // 将后台返回的消息添加到对话历史中
      this.chatHistory.push({
        sender: 'bot',
        message: response.message
      });

      // 更新本地显示的对话历史
      this.saveChatHistoryToLocalStorage();
    },

    // 将对话历史保存到本地存储
    saveChatHistoryToLocalStorage() {
      localStorage.setItem('chatHistory', JSON.stringify(this.chatHistory));
    },

    // 从本地存储加载对话历史
    loadChatHistoryFromLocalStorage() {
      const storedChatHistory = localStorage.getItem('chatHistory');
      if (storedChatHistory) {
        this.chatHistory = JSON.parse(storedChatHistory);
      }
    },

其它的有兴趣的自己研究代码吧。。请大佬点评,互相交流进步。谢谢!

<template>
    <div class="common-layout">
      <el-container>
        <el-card class="box-card">
            <el-header class="model-selection" style="padding: 0">
              <div style="margin-left: 50vw">
                <span>ChatGPT 对话</span>
              </div>
              <el-select v-model="selectedModel" placeholder="请选择模型" @change="modelSelectionChanged">
                <el-option label="GPT-4" value="gpt-4"></el-option>
                <el-option label="GPT-3" value="gpt-3.5-turbo"></el-option>
              </el-select>
              <div style="float: right">
              <el-text class="mx-1" type="success"  style="text-align: right;">用户对话记录最多保存1200字的长度,超过长度将会重置</el-text>
              </div>
              <el-dialog  v-model="showPasswordCard" v-if="selectedModel === 'gpt-4' && showPasswordCard">
                <el-input v-model="password" type="password" placeholder="请输入密码"></el-input>
                <el-button @click="submitPassword" >提交</el-button>
              </el-dialog>
            </el-header>
            <el-aside style="width: 10vw;float: left">
              <el-button style="margin-top: 4vh" @click="clear"   >清空对话记录</el-button>
              <el-button style="display: block;margin-top: 4vh;margin-left: 0" @click="clears"   >使用自己的API</el-button>
            </el-aside>
            <!-- 消息容器 -->
            <div class="message-container" ref="messageContainer">
              <div v-for="(message, index) in messages" :key="index" class="message-item" :class="{ 'user-message': message.isUser, 'bot-message': !message.isUser }">
                <el-card>
                  <template #header>
                    <div v-if="message.isUser" class="card-header">
                      <el-avatar> user </el-avatar>
                      <div>
                      <el-avatar src="https://www.rhzhz.cn/wp-content/uploads/2023/11/20220714222919_c0aa1.thumb_.1000_0.jpg"
                      />
                      </div>
                      </div>
                    <div v-else class="card-header">
                      <div>
                        <el-avatar src="https://www.rhzhz.cn/wp-content/uploads/2022/08/Gravity_Falls_BillCipherCursor.png"
                        />
                      </div>
                      <el-avatar> robot </el-avatar>
                    </div>
                  </template>
                <p v-if="message.isTyping">{{ message.content }}</p>
                  <div v-else-if="message.content" class="text-message" v-html="parseMarkdown(message.content)"></div>
                <p v-else-if="message.error" class="error-message">{{ message.content }}</p>
                </el-card>
                <el-button @click="copyText(message.content)">复制文本</el-button>
              </div>

            </div>
            <div class="chat-input">
              <el-input :disabled="robotIsReplying" v-model="userInput" type="textarea" autosize placeholder="请输入消息"></el-input>
              <el-button :disabled="robotIsReplying" @click="sendMessage" type="primary">发送</el-button>
            </div>
          </el-card>
      </el-container>
    </div>
  </template>
<script>
import axios from 'axios';
import {ElNotification} from "element-plus";
import MarkdownIt from 'markdown-it';
const md = new MarkdownIt();
export default
{
  data() {
    return {
      userInput: '',
      messages: [],password: '',showPasswordCard: false,
      robotIsReplying: false, // 用于标识机器人是否正在回复
      selectedModel: 'gpt-3.5-turbo', // 默认选择 GPT-4
      textToCopy: '要复制的文本内容',
      // 在 data 中添加用于存储用户输入历史的变量
      chatHistory: []  // 用于存储对话历史
    };
  },
  methods: {
    parseMarkdown(text) {
      return md.render(text);
    },
    copyText(text) {
      navigator.clipboard.writeText(text).then(() => {
        alert('文本已成功复制到剪贴板');
      }).catch((error) => {
        alert('文本复制失败:' + error);
      });
    },
    modelSelectionChanged() {
      if (this.selectedModel === 'gpt-4') {
        this.showPasswordCard = true; // 当选择了GPT-4时显示密码输入卡片
      } else {
        this.showPasswordCard = false; // 其他情况下隐藏密码输入卡片
      }
    },
    clear(){
      this.userInput= '',
      this.chatHistory=[] , // 用于存储对话历
      this.messages=[],
          // 调用该函数以清除对话历史
      localStorage.removeItem('chatHistory');
      ElNotification({
        title: 'Success',
        message: '记录已经清空!',
        type: 'success',
      })
},clears(){
      window.location.href = 'https://chatGpt.rhzhz.cn';
    },
    submitPassword() {
      if (this.password === 'useit') { // 替换为真实的密码
        ElNotification({
          title: 'Success',
          message: '权限已经获得!',
          type: 'success',
        })
        this.selectedModel = 'gpt-4'
        this.accessGranted = true; // 设置权限状态为已获得
        this.showPasswordCard = false;
        // this.password = ''; // 清空密码字段
      } else {
        ElNotification({
          title: 'Warning',
          message: '都是贫困惹的祸!',
          type: 'warning',
        })
        // this.selectedModel = 'gpt-4'
        this.selectedModel = 'gpt-3.5-turbo'
        // 密码错误处理
      }
    },
    async sendMessage() {

      if (this.selectedModel === 'gpt-4' && !this.accessGranted) {
        // 弹出提示或其他处理,要求用户先验证密码
        ElNotification({
          title: 'Warning',
          message: '都是贫困惹的祸!',
          type: 'success',
        })
        return;
      }
      console.log("当前模式"+this.selectedModel)
      this.robotIsReplying = true;
      if (this.userInput === ''){this.robotIsReplying = false; return;}
      //特定字符拦截
      if (this.userInput === 'xxx' || this.userInput === 'xxx' || this.userInput === 'xxx') {
        this.robotIsReplying = false;
        this.messages.push({ content: "特定信息!" });
        this.userInput = '';
        return;
      }

      if (this.userInput.trim() === '') return;

      this.messages.push({ content: this.userInput, isUser: true });

      try {
        this.messages.push({ content: "等待机器人回复...", isTyping: true }); // 显示正在输入状态
        // 将用户输入的消息添加到对话历史中
        this.chatHistory.push({
          sender: 'user',
          message: this.userInput
        });

        //在发送消息时将整个对话历史发送给后台
        const historyString = JSON.stringify(this.chatHistory);
        // console.log("历史记录"+this.chatHistory)
        // console.log("机器人说0"+historyString)
        // 提取用户说的消息并拼接成字符串
        // 提取所有用户的消息
        // 假设 chatHistory 是包含完整对话历史的数组
        const maxHistoryLength = 4;  // 指定要保留的最大条目数

        // 裁剪对话历史到最近的 3-4 条信息
        const trimmedChatHistory = this.chatHistory.slice(-maxHistoryLength);
        console.log("裁剪的信息"+trimmedChatHistory)
      // 现在 trimmedChatHistory 中包含了最近的 3-4 条对话历史记录
        const userMessages = trimmedChatHistory
            .filter(item => item.sender === 'user') // 过滤出 sender 为 'user' 的对话记录
            .map(item => item.message); // 通过 map 方法获取每条消息的内容
        // 处理后台返回的响应消息
        // 合并消息为字符串
        const userMessageString = userMessages.join(' '); // 使用 join 方法将消息拼接成一个字符串,用空格分隔每条消息
        // 在拼接的消息后面加上新的问题
        const fullUserMessage = userMessageString + ' 以上是我之前问你的问题,只供你回答我接下来问题做参考,请你不要重复回答我之前问过的问题,只对新问题进行处理。新问题是:' + this.userInput;
        if (userMessageString.length >= 1200) {
          this.chatHistory = []; // 如果用户消息长度超过 1000 个字符,则清空聊天历史
          // 调用该函数以清除对话历史
          localStorage.removeItem('chatHistory');
        }
        // console.log("用户说"+userMessageString)
        console.log("对话内容"+fullUserMessage)
        console.log("机器人说1"+historyString)
        const response = await this.chatGPTRequest(fullUserMessage);

        // while(response.choices[0].message.content)console.log(response.choices[0].message.content)
        this.handleBackendResponse(response);
        console.log("机器人说2"+response)

        const content = response.choices[0].message.content;

        console.log("机器人说3"+content)
        if (content) {
          for (let i = 0; i < content.length; i++) {

            // await this.delay(30); // 等待一段时间再添加下一个字
            this.messages = [...this.messages.slice(0, -1), { content: content.slice(0, i + 1), isUser: false }];

            await this.$nextTick(() => {
              const messageContainer = this.$refs.messageContainer;
              // 滚动到消息容器的底部
              messageContainer.scrollTop = messageContainer.scrollHeight;
            })


          }
          // 将用户输入添加到历史记录中,并清空当前输入

          console.log(response)

        } else {
          this.messages.push({ content: '未能解析回复', isUser: false });
        }

      } catch (error) {
        this.messages = [...this.messages.filter(msg => !msg.isTyping)]; // 清除正在输入状态
        this.messages.push({ content: 'ChatGPT 发生错误', isUser: false, error: true }); // 添加错误消息
      }
      this.userInput = ''; // 清空用户输入
      this.robotIsReplying = false;

    },


    // 处理后台返回的响应消息
    handleBackendResponse(response) {
      // 将后台返回的消息添加到对话历史中
      this.chatHistory.push({
        sender: 'bot',
        message: response.message
      });

      // 更新本地显示的对话历史
      this.saveChatHistoryToLocalStorage();
    },

    // 将对话历史保存到本地存储
    saveChatHistoryToLocalStorage() {
      localStorage.setItem('chatHistory', JSON.stringify(this.chatHistory));
    },

    // 从本地存储加载对话历史
    loadChatHistoryFromLocalStorage() {
      const storedChatHistory = localStorage.getItem('chatHistory');
      if (storedChatHistory) {
        this.chatHistory = JSON.parse(storedChatHistory);
      }
    },
    delay(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    },
    chatGPTRequest(msg) {
      return new Promise((resolve, reject) => {
        axios({
          method: 'post',
          url: 'https://api.openai.com/v1/chat/completions',
          data: {
            "max_tokens": 1200,
            "model": this.selectedModel,
            "temperature": 0.8,
            "top_p": 1,
            "presence_penalty": 1,
            "messages": [
              {
                "role": "system",
                "content": "You are ChatGPT"
              },
              {
                "role": "user",
                "content": msg
              }
            ],
            "stream": false // 设置 stream 为 false
          },
          headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer 你的key'
          },
          onDownloadProgress: (progressEvent) => {
           //数据流
          }
        })
            .then(response => {
             
              resolve(response.data)
            })
            .catch(error => reject(error));
      });
    }
  },
  mounted() {
    // 在组件挂载时加载本地存储中的用户输入历史
    this.loadChatHistoryFromLocalStorage();
  }
};
.common-layout {
  height: 100vh; /* 设置布局高度为整个视口高度 */
  display: flex;
  justify-content: center;
  align-items: center;
}


.user-message, .bot-message {

  margin-bottom: 30px;
}

.user-message {
  /*margin-left: 50vw;*/
  /*background-color: #d4edda; !* 设置用户消息的背景色 *!*/
  text-align: right;
}

.bot-message {
  /*margin-right: 50vw;*/
  text-align: left;
}

.chat-input {
/*position: absolute;*/
  display: flex;
  align-items: center;
  padding: 10px;
  margin-bottom: 0px;
  margin-left: 20vw;
  margin-top: 70vh;
  background-color: #ffffff;
}

.chat-input el-input {
  flex: 1;
  margin-right: 10px;
}
.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.box-card {
  margin:0 auto;
  width: 97vw;
  height: 90vh; /* 注意这里添加了分号 */
  position: relative; /* 让卡片成为定位上下文 */
  /*height: 500px; !* 假设卡片容器有固定高度 *!*/
  overflow: auto; /* 如果需要滚动条的话 */
}
/* 添加打字效果的样式 */
.typing-indicator {
  display: inline-block;
  overflow: hidden;
  position: relative;
  width: 0;
}
.typing-indicator:before {
  display: block;
  content: "|"; /* 使用竖线作为光标 */
  width: 0;
  height: 0;
  position: absolute;
  top: 0;
  right: 0;
  animation: typing 0.5s steps(30, end) infinite; /* 使用 steps 函数来模拟逐字显示 */
}
@keyframes typing {
  from { width: 0; }
  to { width: auto; }
}

.error-message {
  color: red;
  font-style: italic;
}
.text-message {
  margin: 10px 0;

}
@keyframes typing {
  0% {
    height: 0;
  }
  50% {
    height: 1em;
  }
  100% {
    height: 0;
  }
}
/* 可以根据需求自定义样式 */
.chat-container {
  width: 100vw;
  margin: 0 auto;
  display: flex;
  flex-direction: column;
  height: 100vh; /* 设置容器高度为视口高度,保证内容超出时可以滚动 */
}

.message-container {
 
  overflow-y: auto; /* 设置内容超出时显示滚动条 */
  margin-left:  auto;
float: right;
  overflow-y: auto;
  /*float: right;*/
  width: 73vw;
  max-height: 70vh;
  padding: 10px;
}
.user-message {
  text-align: right;
  /*background-color: #d4edda; !* 设置用户消息的背景色 *!*/
}

.bot-message {
  text-align: left;
  /*background-color: #f0f0f0; !* 设置ChatGPT消息的背景色 *!*/
}
.input-container {
  position: absolute; /* 将输入框容器设置为绝对定位 */
  bottom: 0; /* 将输入框容器放置在底部 */
  width: 100%; /* 设置输入框容器的宽度为100% */
  padding: 10px; /* 添加内边距 */
  display: flex; /* 使用 Flex 布局 */
  justify-content: space-between; /* 调整输入框和发送按钮之间的间距 */
}
</style>
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jay叶湘伦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值