From a13c1bc66900620bcd64e7064c3ac8940cb65abd Mon Sep 17 00:00:00 2001 From: RockYang Date: Wed, 19 Apr 2023 17:32:41 +0800 Subject: [PATCH] the function to save user chat history is ready --- server/chat_handler.go | 22 ++-- web/public/images/avatar/yi_yan.png | Bin 0 -> 5148 bytes web/src/components/ChatPrompt.vue | 2 +- web/src/components/ChatReply.vue | 2 +- web/src/main.js | 2 +- web/src/utils/storage.js | 33 ++++- web/src/views/Chat.vue | 3 +- web/src/views/ChatFree.vue | 189 +++++++++++++++++++++------- web/src/views/ChatPlus.vue | 5 +- 9 files changed, 190 insertions(+), 68 deletions(-) create mode 100644 web/public/images/avatar/yi_yan.png diff --git a/server/chat_handler.go b/server/chat_handler.go index bb540596..52f659a6 100644 --- a/server/chat_handler.go +++ b/server/chat_handler.go @@ -61,18 +61,18 @@ func (s *Server) ChatHandle(c *gin.Context) { return } logger.Info("Receive a message: ", string(message)) - //replyMessage(client, "当前 TOKEN 无效,请使用合法的 TOKEN 登录!", false) + replyMessage(client, "当前 TOKEN 无效,请使用合法的 TOKEN 登录!", false) //replyMessage(client, "![](images/wx.png)", true) - ctx, cancel := context.WithCancel(context.Background()) - s.ReqCancelFunc[sessionId] = cancel - // 回复消息 - err = s.sendMessage(ctx, session, chatRole, string(message), client, false) - if err != nil { - logger.Error(err) - } else { - replyChunkMessage(client, types.WsMessage{Type: types.WsEnd, IsHelloMsg: false}) - logger.Info("回答完毕: " + string(message)) - } + //ctx, cancel := context.WithCancel(context.Background()) + //s.ReqCancelFunc[sessionId] = cancel + //// 回复消息 + //err = s.sendMessage(ctx, session, chatRole, string(message), client, false) + //if err != nil { + // logger.Error(err) + //} else { + // replyChunkMessage(client, types.WsMessage{Type: types.WsEnd, IsHelloMsg: false}) + // logger.Info("回答完毕: " + string(message)) + //} } }() diff --git a/web/public/images/avatar/yi_yan.png b/web/public/images/avatar/yi_yan.png new file mode 100644 index 0000000000000000000000000000000000000000..631fbb4f79f9809181aa83421e4764d5137f268b GIT binary patch literal 5148 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 KMNUMnLSTXnQ{1Wm literal 0 HcmV?d00001 diff --git a/web/src/components/ChatPrompt.vue b/web/src/components/ChatPrompt.vue index 763d4150..e1e71784 100644 --- a/web/src/components/ChatPrompt.vue +++ b/web/src/components/ChatPrompt.vue @@ -62,7 +62,7 @@ export default defineComponent({ .content { word-break break-word; - padding: 5px 10px; + padding: 6px 10px; background-color: #98E165; color var(--content-color); font-size: var(--content-font-size); diff --git a/web/src/components/ChatReply.vue b/web/src/components/ChatReply.vue index ca3009f5..5d4f4326 100644 --- a/web/src/components/ChatReply.vue +++ b/web/src/components/ChatReply.vue @@ -97,7 +97,7 @@ export default defineComponent({ .content { min-height 20px; word-break break-word; - padding: 8px 10px; + padding: 6px 10px; color var(--content-color) background-color: #fff; font-size: var(--content-font-size); diff --git a/web/src/main.js b/web/src/main.js index a4f50545..18d3f985 100644 --- a/web/src/main.js +++ b/web/src/main.js @@ -34,7 +34,7 @@ const routes = [ }, { name: 'free', path: '/free', component: ChatFree, meta: { - title: 'ChatGPT 通用免费版' + title: '【文心一言】免费版' } }, { diff --git a/web/src/utils/storage.js b/web/src/utils/storage.js index f5775b0e..f6784b08 100644 --- a/web/src/utils/storage.js +++ b/web/src/utils/storage.js @@ -7,7 +7,8 @@ import Storage from 'good-storage' */ const SessionUserKey = 'LOGIN_USER'; -const ChatHistoryKey = "CHAT_HISTORY"; +const ChatHistoryKey = 'CHAT_HISTORY'; +const ChatListKey = 'CHAT_LIST'; export function getSessionId() { const user = getLoginUser(); @@ -55,8 +56,32 @@ export function appendChatHistory(chatId, message) { // 获取指定会话的历史记录 export function getChatHistory(chatId) { const history = Storage.get(ChatHistoryKey); - if (history && history[chatId]) { - return history[chatId]; + if (!history) { + return null; } - return []; + + return history[chatId] ? history[chatId] : null; +} + +export function getChatList() { + return Storage.get(ChatListKey); +} + +export function getChat(chatId) { + let chatList = Storage.get(ChatListKey); + if (!chatList) { + return null; + } + + return chatList[chatId] ? chatList[chatId] : null; +} + +export function setChat(chat) { + let chatList = Storage.get(ChatListKey); + if (!chatList) { + chatList = {}; + } + + chatList[chat.id] = chat; + Storage.set(ChatListKey, chatList); } diff --git a/web/src/views/Chat.vue b/web/src/views/Chat.vue index 6d595f49..fa67e912 100644 --- a/web/src/views/Chat.vue +++ b/web/src/views/Chat.vue @@ -213,7 +213,7 @@ export default defineComponent({ this.activelyClose = true; this.socket.close(); } - + // 初始化 WebSocket 对象 const sessionId = getSessionId(); const socket = new WebSocket(process.env.VUE_APP_WS_HOST + `/api/chat?sessionId=${sessionId}&role=${this.role}`); @@ -252,7 +252,6 @@ export default defineComponent({ id: randString(32), icon: this.replyIcon, content: "", - cursor: true }); if (data['is_hello_msg'] !== true) { this.canReGenerate = true; diff --git a/web/src/views/ChatFree.vue b/web/src/views/ChatFree.vue index 65556809..d6615082 100644 --- a/web/src/views/ChatFree.vue +++ b/web/src/views/ChatFree.vue @@ -2,13 +2,21 @@ @@ -146,7 +154,15 @@ import {httpGet, httpPost} from "@/utils/http"; import hl from "highlight.js"; import ChatReply from "@/components/ChatReply.vue"; import ChatPrompt from "@/components/ChatPrompt.vue"; -import {getSessionId, getUserInfo, setLoginUser} from "@/utils/storage"; +import { + setChat, + appendChatHistory, + getChatHistory, + getChatList, + getSessionId, + getUserInfo, + setLoginUser +} from "@/utils/storage"; import {ElMessage, ElMessageBox} from "element-plus"; import {isMobile, randString} from "@/utils/libs"; import Clipboard from "clipboard"; @@ -177,12 +193,13 @@ export default defineComponent({ userInfo: {}, showLoginDialog: false, role: 'gpt', - replyIcon: 'images/avatar/gpt.png', // 回复信息的头像 + replyIcon: 'images/avatar/yi_yan.png', // 回复信息的头像 chatList: [], // 会话列表 tmpChatTitle: '', curOpt: '', // 当前操作 curChat: null, // 当前会话 + curPrompt: null, // 当前用户输入 showStopGenerate: false, showReGenerate: false, @@ -202,8 +219,6 @@ export default defineComponent({ }, mounted() { - this.fetchChatHistory(); - const clipboard = new Clipboard('.copy-reply'); clipboard.on('success', () => { ElMessage.success('复制成功!'); @@ -229,7 +244,7 @@ export default defineComponent({ } }); - this.connect(); + this.newChat(); }, @@ -256,7 +271,6 @@ export default defineComponent({ this.errorMessage.close(); // 关闭错误提示信息 } this.activelyClose = false; - // this.chatList.push(this.curChat); // 加载聊天记录 this.fetchChatHistory(); }); @@ -273,7 +287,6 @@ export default defineComponent({ id: randString(32), icon: this.replyIcon, content: "", - cursor: true }); if (data['is_hello_msg'] !== true) { this.canReGenerate = true; @@ -282,9 +295,19 @@ export default defineComponent({ this.sending = false; if (data['is_hello_msg'] !== true) { this.showReGenerate = true; + // 保存聊天记录 + appendChatHistory(this.curChat.id, this.curPrompt); + appendChatHistory(this.curChat.id, { + type: "reply", + id: randString(32), + icon: this.replyIcon, + content: this.lineBuffer, + }) } this.showStopGenerate = false; + this.lineBuffer = ''; // 清空缓冲 + } else { this.lineBuffer += data.content; this.chatData[this.chatData.length - 1]['orgContent'] = this.lineBuffer; @@ -313,7 +336,7 @@ export default defineComponent({ if (this.activelyClose) { // 忽略主动关闭 return; } - + // 停止送消息 this.sending = true; this.checkSession(); @@ -350,33 +373,34 @@ export default defineComponent({ // 从后端获取聊天历史记录 fetchChatHistory: function () { - httpPost("/api/chat/history", {role: this.role}).then((res) => { - if (this.chatData.length > 0) { // 如果已经有聊天记录了,就不追加了 - return - } + const chatList = getChatList(); + if (chatList) { + this.chatList = chatList; - const data = res.data - const md = require('markdown-it')(); - for (let i = 0; i < data.length; i++) { - if (data[i].type === "prompt") { - this.chatData.push(data[i]); - continue; + const list = getChatHistory(this.curChat.id); + if (list) { + const md = require('markdown-it')(); + for (let i = 0; i < list.length; i++) { + if (list[i].type === "prompt") { + this.chatData.push(list[i]); + continue; + } + list[i].orgContent = list[i].content; + list[i].content = md.render(list[i].content); + this.chatData.push(list[i]); } - data[i].orgContent = data[i].content; - data[i].content = md.render(data[i].content); - this.chatData.push(data[i]); + + nextTick(() => { + hl.configure({ignoreUnescapedHTML: true}) + const blocks = document.querySelector("#chat-box").querySelectorAll('pre code'); + blocks.forEach((block) => { + hl.highlightElement(block) + }) + }) } - nextTick(() => { - hl.configure({ignoreUnescapedHTML: true}) - const blocks = document.querySelector("#chat-box").querySelectorAll('pre code'); - blocks.forEach((block) => { - hl.highlightElement(block) - }) - }) - }).catch(() => { - // console.error(e.message) - }) + + } }, inputKeyDown: function (e) { @@ -405,12 +429,13 @@ export default defineComponent({ } // 追加消息 - this.chatData.push({ + this.curPrompt = { type: "prompt", id: randString(32), icon: 'images/avatar/user.png', content: this.inputValue - }); + }; + this.chatData.push(this.curPrompt); this.sending = true; this.showStopGenerate = true; @@ -510,14 +535,25 @@ export default defineComponent({ // 新建会话 newChat: function () { + // 判断当前会话是否已经有聊天记录 + if (this.curChat !== null) { + const chatHistory = getChatHistory(this.curChat.id); + if (chatHistory === null) { + return; + } + // 追加会话 + setChat(this.curChat); + } + this.curChat = { id: randString(32), edit: false, // 是否处于编辑模式 removing: false, // 是否处于删除模式 - title: '新会话 - ' + this.chatList.length + active: false, + title: '新会话 - 0' }; - // 连接会话 + this.chatData = []; this.connect(); }, @@ -533,6 +569,7 @@ export default defineComponent({ if (this.curOpt === 'edit') { chat.title = this.tmpChatTitle; chat.edit = false; + setChat(chat) } else if (this.curOpt === 'remove') { for (let i = 0; i < this.chatList.length; i++) { if (this.chatList[i].id === chat.id) { @@ -551,8 +588,23 @@ export default defineComponent({ removeChat: function (chat) { chat.removing = true; this.curOpt = 'remove'; - } + }, + // 切换会话 + changeChat: function (chat) { + if (this.curChat.id === chat.id) { + return; + } + + this.curChat.active = false; + chat.active = true; + this.curChat = chat; + const chatHistory = getChatHistory(chat.id); + if (!chatHistory) { + return; + } + this.chatData = chatHistory; + } } }) @@ -574,10 +626,51 @@ export default defineComponent({ margin 0 padding 0 width 100% + height calc(100vh - 150px) + border 1px solid red; + overflow-y auto + + .new-chat { + position fixed; + color: #ffffff + padding 10px; + cursor pointer + + &:hover { + background-color #3E3F49 + + a { + .btn { + display block + background-color: #3E3F49; + } + } + } + + a { + border: 1px solid #4A4B4D; + box-sizing: border-box; + border-radius 5px; + width 100%; + display flex; + padding 10px; + + .btn { + display none + right -2px; + top -2px; + + .el-icon { + margin-left 0; + color #ffffff + } + } + } + } ul { list-style-type: none - padding 5px + padding 50px 5px 5px 5px margin 0 li { @@ -589,12 +682,12 @@ export default defineComponent({ border-radius 5px; &:hover { - background-color #2E2F39 + background-color #3E3F49 a { .btn { display block - background-color: #282A32; + background-color: #3E3F49; } } } @@ -647,7 +740,7 @@ export default defineComponent({ } li.active { - background-color #2E2F39 + background-color #3E3F49 } li.new-chat { @@ -669,6 +762,11 @@ export default defineComponent({ } } + + nav::-webkit-scrollbar { + width: 0; + height: 0; + } } .main-content { @@ -707,6 +805,7 @@ export default defineComponent({ justify-content flex-start .chat-box { + width 100%; padding: 10px; overflow-y: auto; height: calc(100vh - 80px); diff --git a/web/src/views/ChatPlus.vue b/web/src/views/ChatPlus.vue index 1a007ede..8fed19ba 100644 --- a/web/src/views/ChatPlus.vue +++ b/web/src/views/ChatPlus.vue @@ -329,8 +329,7 @@ export default defineComponent({ type: "reply", id: randString(32), icon: this.replyIcon, - content: "", - cursor: true + content: "" }); if (data['is_hello_msg'] !== true) { this.canReGenerate = true; @@ -370,7 +369,7 @@ export default defineComponent({ if (this.activelyClose) { // 忽略主动关闭 return; } - + // 停止送消息 this.sending = true; this.checkSession();