From 16d5444e3f1fad93df469fdbc651c5ba97e0d311 Mon Sep 17 00:00:00 2001 From: guo zebin Date: Wed, 3 Dec 2025 20:50:22 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BE=8E=E5=8C=96think=E5=86=85=E5=AE=B9?= =?UTF-8?q?=E7=9A=84ui=E6=95=88=E6=9E=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1、每每条带think内容的消息增加展开窗口; 2、think加载中的效果美。 --- gui/static/css/index.css | 1016 ++++++++++++++------------- gui/static/images/Logo.png | Bin 8882 -> 8851 bytes gui/static/js/index.js | 1320 +++++++++++++++++++----------------- gui/templates/index.html | 270 ++++---- 4 files changed, 1372 insertions(+), 1234 deletions(-) diff --git a/gui/static/css/index.css b/gui/static/css/index.css index f320685..8d57056 100644 --- a/gui/static/css/index.css +++ b/gui/static/css/index.css @@ -1,473 +1,545 @@ -html { - font-size: 14px; -} - - body { - background-image: url(../images/Bg_pic.png); - background-repeat: repeat-x; - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', - 'Droid Sans', 'Helvetica Neue', 'Microsoft Yahei', sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - width: 100%; - height: 100vh; - min-width: 1719px; - overflow: hidden; - } - .main_bg{ - background-image: url(../images/Mid_bg.png); - background-repeat: no-repeat; - background-position: center center; - width: 100%; - margin: 100px auto; - max-width: 1719px; - min-height:790px; - clear: both; - } - - .main_left{ - float: left; - height: 700px; - width: 21%; - padding-top: 30px; - } - - .main_left_logo{ - height: 200px; - text-align: center; - } - - - .main_left_menu{ - margin-top: 10px; - margin-bottom: 1px; - } - .main_left_menu ul{ - list-style-type: none; - margin: 0; - padding-left:9px; - width: 352px; - } - .main_left_menu ul li{ - height: 65px; - margin-top: 15px; - margin-bottom: 15px; - line-height: 52px; - font-size: 20px; - } - .main_left_menu ul li a{ - font-size: 20px; - text-align: center; - display: block; - color: #555; - text-decoration: none;} - - .main_left_menu ul li a:hover { - /* background-color: #f9fbff; */ - color: #0064fb; - /* background-image: url('../images/menu_bg_h.png') no-repeat !important; */ - background-position: center; -} - - .changeImg{ - width: 352px; - height: 65px; - line-height: 65px; - cursor: pointer; - - } - .iconimg1 { - background: url('../images/message.png') no-repeat; - background-size: 32px; - background-position: 100px 50%; - display: block; - text-align: center; - } - .iconimg1:hover{ - background: url('../images/message_h.png') no-repeat; - background-size: 32px; - background-position: 100px 50%; - } - .iconimg2 { - background: url('../images/setting.png') no-repeat; - background-size: 32px; - background-position: 100px 50%; - display: block; - text-align: center; - } - .iconimg3 { - background: url('../images/page3.png') no-repeat; - background-size: 32px; - background-position: 100px 50%; - display: block; - text-align: center; - position: relative; - } - - /* MCP在线状态指示器 */ - .mcp-online-indicator { - position: absolute; - top: 50%; - left: 85px; - width: 10px; - height: 10px; - border-radius: 50%; - background-color: #4CAF50; /* 绿色 */ - margin-top: -5px; - display: none; /* 默认隐藏 */ - } - - /* 当MCP在线时显示指示器 */ - .mcp-online .mcp-online-indicator { - display: block; - } - - .iconimg3:hover{ - background: url('../images/page3_h.png') no-repeat; - background-size: 32px; - background-position: 100px 50%; - } - - .iconimg2:hover{ - background: url('../images/setting_h.png') no-repeat; - background-size: 32px; - background-position: 100px 50%; - } - .changeImg:hover{ - background: url('../images/menu_bg_h.png') no-repeat; - /* z-index: 10; */ - } - - .changeImg2{ - /* width: 352px; */ - height: 65px; - line-height: 65px; - cursor: pointer; - /* background: url('../images/menu_bg_h.png') no-repeat; */ - } - .changeImg2:hover{ - background: url('../images/menu_bg_h.png') no-repeat; - } - - - .changeImg3{ - /* width: 352px; */ - height: 65px; - line-height: 65px; - cursor: pointer; - /* background: url('../images/menu_bg_h.png') no-repeat; */ - } - .changeImg3:hover{ - background: url('../images/menu_bg_h.png') no-repeat; - } - - - - - .main_left_emoji{ - text-align: center; - height: 280px; - background-image: url(../images/emoji_bg.png); - background-repeat: no-repeat; - background-position: center center; - } - - .main_right{float: right;width: 79%;height: 720px;} - -.top_info{font-size: 14px; color: #617bab; line-height: 50px; text-align: left;width: 1000px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } -.top_info_text{font-size: 15px;font-weight: bold;} -.chatmessage{ - padding-bottom: 48px; - overflow-y: scroll; - height: 480px; - margin-bottom: 130px; - z-index: 1; -} - - - -.chat-container { - max-width: 100%; - margin: 20px auto; - padding: 20px; - } - -.message { - display: flex; - margin-bottom: 20px; - } - -.receiver-message { - justify-content: flex-start; - padding-right: 30%; - } - -.sender-message { - justify-content: flex-end; - padding-left: 30%; - } - -.avatar { - width: 52px; - height: 52px; - border-radius: 50%; - margin-right: 15px; - margin-left: 15px; - } - -.message-content { - flex: 0 1 auto; - } - -.message-bubble { - background-color: #FFFFFF; - border-radius: 6px; - padding: 8px; - font-size: 15px; - color: #333; - white-space: pre-wrap; - word-wrap: break-word; - } - -.sender-message.message-bubble { - font-size: 15px; - padding: 8px; - background-color: #008fff; - color: #FFFFFF; - } - -.message-time { - font-size: 12px; - color: #999; - margin-top: 5px; - text-align: left; - } - -.sender-message-time { - font-size: 12px; - color: #999; - margin-top: 5px; - text-align: right; - } - -.Userchange{ - background-color: #FFFFFF; - height: 40px; - font-size: 12px; - border-top: 1px solid #bed1fc; - width: 1358px; /* 设置菜单容器的宽度,可根据实际情况调整 */ - overflow: hidden; /* 隐藏超出容器的内容 */ - position: relative; -} - -.inputmessage{ - margin-left:290px ; - width: 760px; - background: #f9fbff; - border-radius: 70px; - height: 73px; - box-shadow: -10px 0 15px rgba(0, 0, 0, 0.1); - position: absolute; - top: 675px; - z-index: 2; -} - -.inputmessage_voice{ - width: 50px; - float: left; - height: 73px; - padding: 15px 5px 0 20px; -} - -.inputmessage_input{ - background-color: #FFFFFF; - width: 540px; - float: left; - margin-top: 15px; - height: 45px; -} - -.inputmessage_send{ - width: 50px; - float: left; - height: 73px; - padding: 15px 5px 0 15px; -} - -.inputmessage_open{ - width: 60px; - float: right; - height: 73px; - padding: 15px 5px 0 5px; -} - -.text_in{ - width: 540px; - height: 45px; - padding: unset; - outline: unset; - border-style: unset; - background-color: unset; - resize: unset; - font-size: 14px; -} - - .tag-container { background-color: #FFFFFF; - display: flex; - - } - - .tag { - background: url('../images/tabline.png') right no-repeat; - padding: 5px 10px; - font-size: 14px; - cursor: pointer; - color: #617bab;line-height: 30px; - } - - .tag.selected { - background-color: #f4f7ff; - color: #0064fb; - } - #prevButton{background-color: #FFFFFF; border: none; - z-index: 1; - position: absolute; - top: 50%; - transform: translateY(-50%); - } - - #nextButton {background-color: #FFFFFF; border: none; - position: absolute; - top: 50%; - transform: translateY(-50%); - } - - #prevButton { - left: 0; - } - - #nextButton { - right: 0; - } - - .menu-container { - width: 800px; /* 设置菜单容器的宽度,可根据实际情况调整 */ - overflow: hidden; /* 隐藏超出容器的内容 */ - position: relative; - } - - .menu li { - margin-right: 20px; /* 菜单项之间的间距,可调整 */ - } - - .menu li a { - text-decoration: none; - color: black; - } - - .menu { background-color: #FFFFFF; - /* display: flex; */ - white-space: nowrap; - display: flex; - transition: transform 0.3s ease; /* 添加过渡效果,使滑动更平滑 */ - list-style: none; - padding: 0 50px 0 50px; - margin: 0; - display: flex; - transition: transform 0.3s ease; /* 添加过渡效果,使滑动更平滑 */ - } - - .adopt{border: none;background: none;} - - .what-time{vertical-align:top;line-height: 25px;} - - .answer-container { - border: 1px solid #ccc; - padding: 10px; - margin: 10px; - background-color: #f9f9f9; - } - - .adopt-button { - display: inline-block; - cursor: pointer; - position: relative; - } - - .adopt-button img { - width: 21px; - height: 21px; - display: block; - } - - .adopt-button:hover::after { - content: "采纳"; - position: absolute; - top: -30px; - left: 0; - background-color: #000; - color: #fff; - padding: 5px 10px; - border-radius: 4px; - font-size: 12px; - white-space: nowrap; - } -.think-panel-container { - position: fixed; - top: 100px; /* 保证不与顶部导航栏重叠 */ - right: 120px; - width: 250px; - height: auto; - padding: 10px; - background: #fff; - border: 1px solid #ccc; - border-radius: 4px; - z-index: 1000; - } - .think-panel { - margin-top: 10px; - padding: 10px; - background: #f9f9f9; - border: 1px dashed #ccc; - border-radius: 4px; - max-height: 620px; - overflow-y: auto; - white-space: pre-wrap; /* 保持原始格式 */ - } - -.think-panel-header { - display: flex; - justify-content: space-between; - align-items: center; - padding-right: 10px; -} - -.think-panel-minimize { - background: none; - border: none; - color: #617bab; - cursor: pointer; - padding: 5px; - line-height: 1; - display: flex; - align-items: center; - justify-content: center; -} - -.think-panel-minimize:hover { - color: #0064fb; -} - -.think-panel-icon { - font-size: 24px; - display: inline-block; - line-height: 1; - font-weight: bold; - color: #ff0000; -} - -.think-panel.minimized { - height: 40px; - overflow: hidden; +html { + font-size: 14px; +} + + body { + background-image: url(../images/Bg_pic.png); + background-repeat: repeat-x; + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', + 'Droid Sans', 'Helvetica Neue', 'Microsoft Yahei', sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + width: 100%; + height: 100vh; + min-width: 1719px; + overflow: hidden; + } + .main_bg{ + background-image: url(../images/Mid_bg.png); + background-repeat: no-repeat; + background-position: center center; + width: 100%; + margin: 100px auto; + max-width: 1719px; + min-height:790px; + clear: both; + } + + .main_left{ + float: left; + height: 700px; + width: 21%; + padding-top: 30px; + } + + .main_left_logo{ + height: 200px; + text-align: center; + } + + + .main_left_menu{ + margin-top: 10px; + margin-bottom: 1px; + } + .main_left_menu ul{ + list-style-type: none; + margin: 0; + padding-left:9px; + width: 352px; + } + .main_left_menu ul li{ + height: 65px; + margin-top: 15px; + margin-bottom: 15px; + line-height: 52px; + font-size: 20px; + } + .main_left_menu ul li a{ + font-size: 20px; + text-align: center; + display: block; + color: #555; + text-decoration: none;} + + .main_left_menu ul li a:hover { + /* background-color: #f9fbff; */ + color: #0064fb; + /* background-image: url('../images/menu_bg_h.png') no-repeat !important; */ + background-position: center; +} + + .changeImg{ + width: 352px; + height: 65px; + line-height: 65px; + cursor: pointer; + + } + .iconimg1 { + background: url('../images/message.png') no-repeat; + background-size: 32px; + background-position: 100px 50%; + display: block; + text-align: center; + } + .iconimg1:hover{ + background: url('../images/message_h.png') no-repeat; + background-size: 32px; + background-position: 100px 50%; + } + .iconimg2 { + background: url('../images/setting.png') no-repeat; + background-size: 32px; + background-position: 100px 50%; + display: block; + text-align: center; + } + .iconimg3 { + background: url('../images/page3.png') no-repeat; + background-size: 32px; + background-position: 100px 50%; + display: block; + text-align: center; + position: relative; + } + + /* MCP在线状态指示器 */ + .mcp-online-indicator { + position: absolute; + top: 50%; + left: 85px; + width: 10px; + height: 10px; + border-radius: 50%; + background-color: #4CAF50; /* 绿色 */ + margin-top: -5px; + display: none; /* 默认隐藏 */ + } + + /* 当MCP在线时显示指示器 */ + .mcp-online .mcp-online-indicator { + display: block; + } + + .iconimg3:hover{ + background: url('../images/page3_h.png') no-repeat; + background-size: 32px; + background-position: 100px 50%; + } + + .iconimg2:hover{ + background: url('../images/setting_h.png') no-repeat; + background-size: 32px; + background-position: 100px 50%; + } + .changeImg:hover{ + background: url('../images/menu_bg_h.png') no-repeat; + /* z-index: 10; */ + } + + .changeImg2{ + /* width: 352px; */ + height: 65px; + line-height: 65px; + cursor: pointer; + /* background: url('../images/menu_bg_h.png') no-repeat; */ + } + .changeImg2:hover{ + background: url('../images/menu_bg_h.png') no-repeat; + } + + + .changeImg3{ + /* width: 352px; */ + height: 65px; + line-height: 65px; + cursor: pointer; + /* background: url('../images/menu_bg_h.png') no-repeat; */ + } + .changeImg3:hover{ + background: url('../images/menu_bg_h.png') no-repeat; + } + + + + + .main_left_emoji{ + text-align: center; + height: 280px; + background-image: url(../images/emoji_bg.png); + background-repeat: no-repeat; + background-position: center center; + } + + .main_right{float: right;width: 79%;height: 720px;} + +.top_info{font-size: 14px; color: #617bab; line-height: 50px; text-align: left;width: 1000px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } +.top_info_text{font-size: 15px;font-weight: bold;} +.chatmessage{ + padding-bottom: 48px; + overflow-y: scroll; + height: 480px; + margin-bottom: 130px; + z-index: 1; +} + + + +.chat-container { + max-width: 100%; + margin: 20px auto; + padding: 20px; + } + +.message { + display: flex; + margin-bottom: 20px; + } + +.receiver-message { + justify-content: flex-start; + padding-right: 30%; + } + +.sender-message { + justify-content: flex-end; + padding-left: 30%; + } + +.avatar { + width: 52px; + height: 52px; + border-radius: 50%; + margin-right: 15px; + margin-left: 15px; + } + +.message-content { + flex: 0 1 auto; + } + +.message-bubble { + background-color: #FFFFFF; + border-radius: 6px; + padding: 8px; + font-size: 15px; + color: #333; + white-space: pre-wrap; + word-wrap: break-word; + } + +.sender-message.message-bubble { + font-size: 15px; + padding: 8px; + background-color: #008fff; + color: #FFFFFF; + } + +.message-time { + font-size: 12px; + color: #999; + margin-top: 5px; + text-align: left; + } + +.sender-message-time { + font-size: 12px; + color: #999; + margin-top: 5px; + text-align: right; + } + +.Userchange{ + background-color: #FFFFFF; + height: 40px; + font-size: 12px; + border-top: 1px solid #bed1fc; + width: 1358px; /* 设置菜单容器的宽度,可根据实际情况调整 */ + overflow: hidden; /* 隐藏超出容器的内容 */ + position: relative; +} + +.inputmessage{ + margin-left:290px ; + width: 760px; + background: #f9fbff; + border-radius: 70px; + height: 73px; + box-shadow: -10px 0 15px rgba(0, 0, 0, 0.1); + position: absolute; + top: 675px; + z-index: 2; +} + +.inputmessage_voice{ + width: 50px; + float: left; + height: 73px; + padding: 15px 5px 0 20px; +} + +.inputmessage_input{ + background-color: #FFFFFF; + width: 540px; + float: left; + margin-top: 15px; + height: 45px; +} + +.inputmessage_send{ + width: 50px; + float: left; + height: 73px; + padding: 15px 5px 0 15px; +} + +.inputmessage_open{ + width: 60px; + float: right; + height: 73px; + padding: 15px 5px 0 5px; +} + +.text_in{ + width: 540px; + height: 45px; + padding: unset; + outline: unset; + border-style: unset; + background-color: unset; + resize: unset; + font-size: 14px; +} + + .tag-container { background-color: #FFFFFF; + display: flex; + + } + + .tag { + background: url('../images/tabline.png') right no-repeat; + padding: 5px 10px; + font-size: 14px; + cursor: pointer; + color: #617bab;line-height: 30px; + } + + .tag.selected { + background-color: #f4f7ff; + color: #0064fb; + } + #prevButton{background-color: #FFFFFF; border: none; + z-index: 1; + position: absolute; + top: 50%; + transform: translateY(-50%); + } + + #nextButton {background-color: #FFFFFF; border: none; + position: absolute; + top: 50%; + transform: translateY(-50%); + } + + #prevButton { + left: 0; + } + + #nextButton { + right: 0; + } + + .menu-container { + width: 800px; /* 设置菜单容器的宽度,可根据实际情况调整 */ + overflow: hidden; /* 隐藏超出容器的内容 */ + position: relative; + } + + .menu li { + margin-right: 20px; /* 菜单项之间的间距,可调整 */ + } + + .menu li a { + text-decoration: none; + color: black; + } + + .menu { background-color: #FFFFFF; + /* display: flex; */ + white-space: nowrap; + display: flex; + transition: transform 0.3s ease; /* 添加过渡效果,使滑动更平滑 */ + list-style: none; + padding: 0 50px 0 50px; + margin: 0; + display: flex; + transition: transform 0.3s ease; /* 添加过渡效果,使滑动更平滑 */ + } + + .adopt{border: none;background: none;} + + .what-time{vertical-align:top;line-height: 25px;} + + .answer-container { + border: 1px solid #ccc; + padding: 10px; + margin: 10px; + background-color: #f9f9f9; + } + + .adopt-button { + display: inline-block; + cursor: pointer; + position: relative; + } + + .adopt-button img { + width: 21px; + height: 21px; + display: block; + } + + .adopt-button:hover::after { + content: "采纳"; + position: absolute; + top: -30px; + left: 0; + background-color: #000; + color: #fff; + padding: 5px 10px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + } +.think-panel-container { + position: fixed; + top: 100px; /* 保证不与顶部导航栏重叠 */ + right: 120px; + width: 250px; + height: auto; + padding: 10px; + background: #fff; + border: 1px solid #ccc; + border-radius: 4px; + z-index: 1000; + } + .think-panel { + margin-top: 10px; + padding: 10px; + background: #f9f9f9; + border: 1px dashed #ccc; + border-radius: 4px; + max-height: 620px; + overflow-y: auto; + white-space: pre-wrap; /* 保持原始格式 */ + } + +.think-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-right: 10px; +} + +.think-panel-minimize { + background: none; + border: none; + color: #617bab; + cursor: pointer; + padding: 5px; + line-height: 1; + display: flex; + align-items: center; + justify-content: center; +} + +.think-panel-minimize:hover { + color: #0064fb; +} + +.think-panel-icon { + font-size: 24px; + display: inline-block; + line-height: 1; + font-weight: bold; + color: #ff0000; +} + +.think-panel.minimized { + height: 40px; + overflow: hidden; +} + +/* 消息内 think 折叠样式 */ +.think-toggle { + display: flex; + align-items: center; + cursor: pointer; + padding: 4px 8px; + margin-bottom: 6px; + background-color: #f0f4ff; + border-radius: 4px; + border: 1px solid #d0d8e8; + user-select: none; +} + +.think-toggle:hover { + background-color: #e0e8ff; +} + +.think-arrow { + display: inline-block; + font-size: 10px; + margin-right: 6px; + color: #617bab; + transition: transform 0.2s ease; +} + +.think-arrow.expanded { + transform: rotate(90deg); +} + +.think-label { + font-size: 12px; + color: #617bab; + font-weight: 500; +} + +.think-spinner { + display: inline-block; + width: 12px; + height: 12px; + margin-left: 6px; + border: 2px solid #d0d8e8; + border-top-color: #617bab; + border-radius: 50%; + animation: think-spin 0.8s linear infinite; +} + +@keyframes think-spin { + to { + transform: rotate(360deg); + } +} + +.think-content-inline { + background-color: #f5f7fa; + border: 1px dashed #ccc; + border-radius: 4px; + padding: 8px 10px; + margin-bottom: 8px; + font-size: 12px; + color: #666; + white-space: pre-line; + word-wrap: break-word; + max-height: 300px; + overflow-y: auto; + line-height: 1.5; +} + +.message-text { + white-space: pre-wrap; + word-wrap: break-word; } \ No newline at end of file diff --git a/gui/static/images/Logo.png b/gui/static/images/Logo.png index 837fcb58ac38dfcaa1d514eb098d5800d389c980..669339eca4d69191f210fa177a4f3f646f58012b 100644 GIT binary patch delta 8697 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 G(EkDFkN3|2 delta 8721 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 { - console.log('WebSocket connection opened'); - }; - - this.websocket.onmessage = (event) => { - const data = JSON.parse(event.data); - this.handleIncomingMessage(data); - }; - - this.websocket.onclose = () => { - console.log('WebSocket connection closed. Attempting to reconnect...'); - setTimeout(() => this.connectWebSocket(), 5000); - }; - - this.websocket.onerror = (error) => { - console.error('WebSocket error:', error); - }; - } - - async fetchData(url, options = {}) { - try { - // Ensure headers are properly set for POST requests - if (options.method === 'POST') { - options.headers = { - 'Content-Type': 'application/json', - ...options.headers - }; - } - - const response = await fetch(url, options); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - const data = await response.json(); - return data; - } catch (error) { - console.error('Error fetching data:', error); - throw error; // Rethrow to handle in the calling function - } - } - - getVoiceList() { - return this.fetchData(`${this.baseApiUrl}/api/get-voice-list`); - } - - getAudioDeviceList() { - return this.fetchData(`${this.baseApiUrl}/api/get-audio-device-list`); - } - - submitConfig(config) { - return this.fetchData(`${this.baseApiUrl}/api/submit`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ config }) - }); - } - - controlEyes(state) { - return this.fetchData(`${this.baseApiUrl}/api/control-eyes`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ state }) - }); - } - - startLive() { - return this.fetchData(`${this.baseApiUrl}/api/start-live`, { - method: 'POST' - }); - } - - stopLive() { - return this.fetchData(`${this.baseApiUrl}/api/stop-live`, { - method: 'POST' - }); - } - - getRunStatus() { - return this.fetchData(`${this.baseApiUrl}/api/get-run-status`, { - method: 'POST' - }); - } - - getMessageHistory(username) { - return new Promise((resolve, reject) => { - const url = `${this.baseApiUrl}/api/get-msg`; - const xhr = new XMLHttpRequest(); - xhr.open("POST", url); - xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); - const send_data = `data=${encodeURIComponent(JSON.stringify({ username }))}`; - xhr.send(send_data); - - xhr.onreadystatechange = function () { - if (xhr.readyState === 4) { - if (xhr.status === 200) { - try { - const data = JSON.parse(xhr.responseText); - if (data && data.list) { - const combinedList = data.list.flat(); - resolve(combinedList); - } else { - resolve([]); - } - } catch (e) { - console.error('Error parsing response:', e); - reject(e); - } - } else { - reject(new Error(`Request failed with status ${xhr.status}`)); - } - } - }; - }); - } - - getUserList() { - return this.fetchData(`${this.baseApiUrl}/api/get-member-list`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }); - } - - getData() { - return this.fetchData(`${this.baseApiUrl}/api/get-data`, { - method: 'POST', - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - }); -} - - getTime(){ - const date = new Date(); - const year = date.getFullYear(); - const month = (date.getMonth() + 1).toString().padStart(2, '0'); // 月份从0开始,需要+1 - const day = date.getDate().toString().padStart(2, '0'); - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - const seconds = date.getSeconds().toString().padStart(2, '0'); - const milliseconds = date.getMilliseconds().toString().padStart(3, '0'); - const currentDateTimeWithMs = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`; - return currentDateTimeWithMs - } - - handleIncomingMessage(data) { - const vueInstance = this.vueInstance; - // console.log('Incoming message:', data); - if (data.liveState !== undefined) { - vueInstance.liveState = data.liveState; - if (data.liveState === 1) { - vueInstance.configEditable = false; - } else if (data.liveState === 0) { - vueInstance.configEditable = true; - } - } - - if (data.voiceList !== undefined) { - vueInstance.voiceList = data.voiceList.map(voice => ({ - value: voice.id, - label: voice.name - })); - } - - if (data.deviceList !== undefined) { - vueInstance.deviceList = data.deviceList.map(device => ({ - value: device, - label: device - })); - } - - if (data.panelMsg !== undefined) { - vueInstance.panelMsg = data.panelMsg; - - } - if (data.robot) { - console.log(data.robot) - vueInstance.$set(vueInstance, 'robot', data.robot); - } - if (data.panelReply !== undefined) { - console.log('收到消息:', data.panelReply); - vueInstance.panelReply = data.panelReply.content; - - // 更新用户列表 - const userExists = vueInstance.userList.some(user => user[1] === data.panelReply.username); - if (!userExists) { - vueInstance.userList.push([data.panelReply.uid, data.panelReply.username]); - } - - if (vueInstance.selectedUser && data.panelReply.username === vueInstance.selectedUser[1]) { - // 查找是否已存在相同content_id的消息 - const existingMessageIndex = vueInstance.messages.findIndex( - msg => msg.id === data.panelReply.id && msg.type === data.panelReply.type - ); - - if (existingMessageIndex !== -1) { - // 更新现有消息(拼接内容) - const existingMessage = vueInstance.messages[existingMessageIndex]; - // 拼接新内容到现有内容 - existingMessage.content = existingMessage.content + data.panelReply.content; - existingMessage.timetext = this.getTime(); - // 强制更新视图 - vueInstance.$forceUpdate(); - } else { - // 添加新消息 - vueInstance.messages.push({ - id: data.panelReply.id, - username: data.panelReply.username, - content: data.panelReply.content, - type: data.panelReply.type, - timetext: this.getTime(), - is_adopted: data.panelReply.is_adopted ? 1 : 0 - }); - } - - // 滚动到底部 - vueInstance.$nextTick(() => { - const chatContainer = vueInstance.$el.querySelector('.chatmessage'); - if (chatContainer) { - chatContainer.scrollTop = chatContainer.scrollHeight; - } - }); - } - } - - if (data.is_connect !== undefined) { - vueInstance.isConnected = data.is_connect; - } - - if (data.remote_audio_connect !== undefined) { - vueInstance.remoteAudioConnected = data.remote_audio_connect; - } - } -} - -new Vue({ - el: '#app', - delimiters: ["[[", "]]"], - data() { - return { - messages: [], - newMessage: '', - fayService: null, - liveState: 0, - isConnected: false, - remoteAudioConnected: false, - userList: [], - selectedUser: null, - loading: false, - chatMessages: {}, - panelMsg: '', - panelReply: '', - robot:'static/images/Normal.gif', - base_url: window.location.protocol + '//' + window.location.hostname + ':' + window.location.port, - hostname: window.location.hostname, - play_sound_enabled: false, - source_record_enabled: false, - userListTimer: null, - thinkPanelExpanded: true, - thinkContent: '', - isThinkPanelMinimized: false, - mcpOnlineStatus: false, - mcpCheckTimer: null, - }; - }, - watch: { - messages: { - handler(newMessages) { - for (let i = newMessages.length - 1; i >= 0; i--) { - let msg = newMessages[i]; - if (msg.type === 'fay') { - const regex = /([\s\S]*?)<\/think>/; - const match = msg.content.match(regex); - if (match && match[1]) { - this.thinkContent = match[1]; - // 从原始消息中移除think标签及其内容,并去除多余空格 - msg.content = msg.content.replace(regex, '').trim(); - break; - } - } - } - }, - deep: true - } - }, - created() { - this.initFayService(); - this.getData(); - this.startUserListTimer(); - this.checkMcpStatus(); - this.startMcpStatusTimer(); - }, - methods: { - initFayService() { - const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - const wsHost = window.location.hostname; - const wsUrl = `${wsProtocol}//${wsHost}:10003`; - this.fayService = new FayInterface(wsUrl, this.base_url, this); - this.fayService.connectWebSocket(); - this.fayService.websocket.addEventListener('open', () => { - this.loadUserList(); - }); - }, - async loadUserList() { - try { - const result = await this.fayService.getUserList(); - if (result && result.list) { - this.userList = result.list; - if (this.userList.length > 0) { - this.selectedUser = this.userList[0]; - await this.loadMessageHistory(this.selectedUser[1]); - } - } - } catch (error) { - console.error('Failed to load user list:', error); - this.$message.error('Failed to load user list. Please try again.'); - } - }, - sendMessage() { - let _this = this; - let text = _this.newMessage; - if (!text) { - alert('请输入内容'); - return; - } - if (_this.selectedUser === 'others' && !_this.othersUser) { - alert('请输入自定义用户名'); - return; - } - if (this.liveState != 1) { - alert('请先开启服务'); - return; - } - let usernameToSend = _this.selectedUser === 'others' ? _this.othersUser : _this.selectedUser[1]; - - this.timer = setTimeout(() => { - let height = document.querySelector('.chatmessage').scrollHeight; - document.querySelector('.chatmessage').scrollTop = height; - }, 1000); - _this.newMessage = ''; - let url = `${this.base_url}/api/send`; - let send_data = { - "msg": text, - "username": usernameToSend - }; - - let xhr = new XMLHttpRequest(); - xhr.open("post", url); - xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); - xhr.send('data=' + encodeURIComponent(JSON.stringify(send_data))); - let executed = false; - xhr.onreadystatechange = async function () { - if (!executed && xhr.status === 200) { - executed = true; - } - }; - }, - getData() { - this.fayService.getRunStatus().then((data) => { - if (data) { - if(data.status){ - this.liveState = 1; - this.configEditable = false; - }else{ - this.liveState = 0; - this.configEditable = true; - } - - } - }); - this.fayService.getData().then((data) => { - if (data) { - this.updateConfigFromData(data.config); - } - }); - }, - updateConfigFromData(config) { - - if (config.interact) { - this.play_sound_enabled = config.interact.playSound; - } - if (config.source && config.source.record) { - this.source_record_enabled = config.source.record.enabled; - } - }, - saveConfig() { - let url = `${this.base_url}/api/submit`; - let send_data = { - "config": { - "source": { - "record": { - "enabled": this.source_record_enabled, - }, - }, - "interact": { - "playSound": this.play_sound_enabled, - } - } - }; - - let xhr = new XMLHttpRequest() - xhr.open("post", url) - xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded") - xhr.send('data=' + JSON.stringify(send_data)) - let executed = false - xhr.onreadystatechange = async function () { - if (!executed && xhr.status === 200) { - try { - let data = await eval('(' + xhr.responseText + ')') - executed = true - } catch (e) { - } - } - } -}, - changeRecord(){ - if(this.source_record_enabled){ - this.source_record_enabled = false - }else{ - this.source_record_enabled = true - } - this.saveConfig() - }, - changeSound(){ - if(this.play_sound_enabled){ - this.play_sound_enabled = false - }else{ - this.play_sound_enabled = true - } - this.saveConfig() - }, - loadUserList() { - this.fayService.getUserList().then((response) => { - if (response && response.list) { - if (response.list.length == 0){ - // 检查是否已经有默认用户 - const defaultUserExists = this.userList.some(user => user[1] === 'User'); - if (!defaultUserExists) { - // 只有在不存在默认用户时才添加 - const info = []; - info[0] = 1; - info[1] = 'User'; - this.userList.push(info); - this.selectUser(info); - console.log('添加默认用户: User'); - } - } else { - this.userList = response.list; - if (!this.selectedUser) { - this.selectUser(this.userList[0]); - } - } - } - }); - }, - startUserListTimer() { - // 清除可能存在的旧定时器 - if (this.userListTimer) { - clearInterval(this.userListTimer); - } - // 设置新的定时器,每30秒执行一次 - this.userListTimer = setInterval(() => { - this.loadUserList(); - }, 30000); - }, - // 组件销毁时清除定时器 - beforeDestroy() { - if (this.userListTimer) { - clearInterval(this.userListTimer); - this.userListTimer = null; - } - if (this.mcpCheckTimer) { - clearInterval(this.mcpCheckTimer); - this.mcpCheckTimer = null; - } - }, - selectUser(user) { - this.selectedUser = user; - this.fayService.websocket.send(JSON.stringify({ "Username": user[1] })); - this.loadMessageHistory(user[1], 'common'); - }, - startLive() { - this.liveState = 2 - this.fayService.startLive().then(() => { - this.sendSuccessMsg('已开启!'); - this.getData(); - }); - }, - stopLive() { - this.fayService.stopLive().then(() => { - this.liveState = 3 - this.sendSuccessMsg('已关闭!'); - }); - }, - - loadMessageHistory(username, type) { - this.fayService.getMessageHistory(username).then((response) => { - if (response) { - this.messages = response; - if(type == 'common'){ - this.$nextTick(() => { - const chatContainer = document.querySelector('.chatmessage'); - if (chatContainer) { - chatContainer.scrollTop = chatContainer.scrollHeight; - } - }); - } - } - }); - }, - sendSuccessMsg(message) { - this.$notify({ - title: '成功', - message, - type: 'success', - }); - -} , -adoptText(id) { -// 调用采纳接口 -this.fayService.fetchData(`${this.base_url}/api/adopt-msg`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ id }) -}) -.then((response) => { - if (response && response.status === 'success') { - // 处理成功的响应 - this.$notify({ - title: '成功', - message: response.msg, // 显示成功消息 - type: 'success', - }); - - this.loadMessageHistory(this.selectedUser[1], 'adopt'); - } else { - // 处理失败的响应 - this.$notify({ - title: '失败', - message: response ? response.msg : '请求失败', - type: 'error', - }); - } -}) -.catch((error) => { - // 处理网络错误或HTTP错误 - this.$notify({ - title: '错误', - message: error.message || '请求失败', - type: 'error', - }); -}); -} -, - minimizeThinkPanel() { - this.isThinkPanelMinimized = !this.isThinkPanelMinimized; - const panel = document.querySelector('.think-panel'); - panel.classList.toggle('minimized'); - }, - - // 检查MCP服务器状态 - checkMcpStatus() { - const mcpUrl = `http://${this.hostname}:5010/api/mcp/servers`; - - // 使用超时设置的fetch请求 - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 3000); // 3秒超时 - - fetch(mcpUrl, { signal: controller.signal }) - .then(response => { - clearTimeout(timeoutId); - if (!response.ok) { - throw new Error('MCP服务器响应不正常'); - } - return response.json(); - }) - .then(data => { - if (Array.isArray(data)) { - // 检查是否有任何一个MCP服务器在线 - const hasOnlineServer = data.some(server => server.status === 'online'); - this.mcpOnlineStatus = hasOnlineServer; - } else { - console.warn('MCP服务器返回的数据格式不正确'); - this.mcpOnlineStatus = false; - } - }) - .catch(error => { - clearTimeout(timeoutId); - // 如果是超时错误,不输出详细错误信息 - if (error.name === 'AbortError') { - console.warn('MCP服务器请求超时'); - } else { - console.warn('检查MCP状态出错:', error.message); - } - this.mcpOnlineStatus = false; - }); - }, - - // 启动MCP状态检查定时器 - startMcpStatusTimer() { - // 清除可能存在的旧定时器 - if (this.mcpCheckTimer) { - clearInterval(this.mcpCheckTimer); - } - // 设置新的定时器,每30秒检查一次MCP状态 - this.mcpCheckTimer = setInterval(() => { - this.checkMcpStatus(); - }, 30000); - }, - - - } -}); +// fayApp.js +class FayInterface { + constructor(baseWsUrl, baseApiUrl, vueInstance) { + this.baseWsUrl = baseWsUrl; + this.baseApiUrl = baseApiUrl; + this.websocket = null; + this.vueInstance = vueInstance; + } + + connectWebSocket() { + if (this.websocket) { + this.websocket.onopen = null; + this.websocket.onmessage = null; + this.websocket.onclose = null; + this.websocket.onerror = null; + } + + this.websocket = new WebSocket(this.baseWsUrl); + + this.websocket.onopen = () => { + console.log('WebSocket connection opened'); + }; + + this.websocket.onmessage = (event) => { + const data = JSON.parse(event.data); + this.handleIncomingMessage(data); + }; + + this.websocket.onclose = () => { + console.log('WebSocket connection closed. Attempting to reconnect...'); + setTimeout(() => this.connectWebSocket(), 5000); + }; + + this.websocket.onerror = (error) => { + console.error('WebSocket error:', error); + }; + } + + async fetchData(url, options = {}) { + try { + // Ensure headers are properly set for POST requests + if (options.method === 'POST') { + options.headers = { + 'Content-Type': 'application/json', + ...options.headers + }; + } + + const response = await fetch(url, options); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const data = await response.json(); + return data; + } catch (error) { + console.error('Error fetching data:', error); + throw error; // Rethrow to handle in the calling function + } + } + + getVoiceList() { + return this.fetchData(`${this.baseApiUrl}/api/get-voice-list`); + } + + getAudioDeviceList() { + return this.fetchData(`${this.baseApiUrl}/api/get-audio-device-list`); + } + + submitConfig(config) { + return this.fetchData(`${this.baseApiUrl}/api/submit`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ config }) + }); + } + + controlEyes(state) { + return this.fetchData(`${this.baseApiUrl}/api/control-eyes`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ state }) + }); + } + + startLive() { + return this.fetchData(`${this.baseApiUrl}/api/start-live`, { + method: 'POST' + }); + } + + stopLive() { + return this.fetchData(`${this.baseApiUrl}/api/stop-live`, { + method: 'POST' + }); + } + + getRunStatus() { + return this.fetchData(`${this.baseApiUrl}/api/get-run-status`, { + method: 'POST' + }); + } + + getMessageHistory(username) { + return new Promise((resolve, reject) => { + const url = `${this.baseApiUrl}/api/get-msg`; + const xhr = new XMLHttpRequest(); + xhr.open("POST", url); + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + const send_data = `data=${encodeURIComponent(JSON.stringify({ username }))}`; + xhr.send(send_data); + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + if (xhr.status === 200) { + try { + const data = JSON.parse(xhr.responseText); + if (data && data.list) { + const combinedList = data.list.flat(); + resolve(combinedList); + } else { + resolve([]); + } + } catch (e) { + console.error('Error parsing response:', e); + reject(e); + } + } else { + reject(new Error(`Request failed with status ${xhr.status}`)); + } + } + }; + }); + } + + getUserList() { + return this.fetchData(`${this.baseApiUrl}/api/get-member-list`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + } + + getData() { + return this.fetchData(`${this.baseApiUrl}/api/get-data`, { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + }); +} + + getTime(){ + const date = new Date(); + const year = date.getFullYear(); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); // 月份从0开始,需要+1 + const day = date.getDate().toString().padStart(2, '0'); + const hours = date.getHours().toString().padStart(2, '0'); + const minutes = date.getMinutes().toString().padStart(2, '0'); + const seconds = date.getSeconds().toString().padStart(2, '0'); + const milliseconds = date.getMilliseconds().toString().padStart(3, '0'); + const currentDateTimeWithMs = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`; + return currentDateTimeWithMs + } + + handleIncomingMessage(data) { + const vueInstance = this.vueInstance; + // console.log('Incoming message:', data); + if (data.liveState !== undefined) { + vueInstance.liveState = data.liveState; + if (data.liveState === 1) { + vueInstance.configEditable = false; + } else if (data.liveState === 0) { + vueInstance.configEditable = true; + } + } + + if (data.voiceList !== undefined) { + vueInstance.voiceList = data.voiceList.map(voice => ({ + value: voice.id, + label: voice.name + })); + } + + if (data.deviceList !== undefined) { + vueInstance.deviceList = data.deviceList.map(device => ({ + value: device, + label: device + })); + } + + if (data.panelMsg !== undefined) { + vueInstance.panelMsg = data.panelMsg; + + } + if (data.robot) { + console.log(data.robot) + vueInstance.$set(vueInstance, 'robot', data.robot); + } + if (data.panelReply !== undefined) { + console.log('收到消息:', data.panelReply); + vueInstance.panelReply = data.panelReply.content; + + // 更新用户列表 + const userExists = vueInstance.userList.some(user => user[1] === data.panelReply.username); + if (!userExists) { + vueInstance.userList.push([data.panelReply.uid, data.panelReply.username]); + } + + if (vueInstance.selectedUser && data.panelReply.username === vueInstance.selectedUser[1]) { + // 查找是否已存在相同content_id的消息 + const existingMessageIndex = vueInstance.messages.findIndex( + msg => msg.id === data.panelReply.id && msg.type === data.panelReply.type + ); + + if (existingMessageIndex !== -1) { + // 更新现有消息(拼接内容) + const existingMessage = vueInstance.messages[existingMessageIndex]; + // 拼接新内容到现有内容 + existingMessage.content = existingMessage.content + data.panelReply.content; + existingMessage.timetext = this.getTime(); + + // 检测 think 标签状态 + const hasThinkStart = existingMessage.content.includes(''); + const hasThinkEnd = existingMessage.content.includes(''); + if (hasThinkStart && !hasThinkEnd) { + // think 正在接收中,展开并显示加载状态 + vueInstance.$set(existingMessage, 'thinkExpanded', true); + vueInstance.$set(existingMessage, 'thinkLoading', true); + } else if (hasThinkStart && hasThinkEnd) { + // think 接收完成,关闭加载状态 + vueInstance.$set(existingMessage, 'thinkLoading', false); + } + + // 强制更新视图 + vueInstance.$forceUpdate(); + } else { + // 添加新消息 + const newMessage = { + id: data.panelReply.id, + username: data.panelReply.username, + content: data.panelReply.content, + type: data.panelReply.type, + timetext: this.getTime(), + is_adopted: data.panelReply.is_adopted ? 1 : 0, + thinkExpanded: false, + thinkLoading: false + }; + + // 检测新消息是否包含 think 开始标签 + if (newMessage.content.includes('') && !newMessage.content.includes('')) { + newMessage.thinkExpanded = true; + newMessage.thinkLoading = true; + } + + vueInstance.messages.push(newMessage); + } + + // 滚动到底部 + vueInstance.$nextTick(() => { + const chatContainer = vueInstance.$el.querySelector('.chatmessage'); + if (chatContainer) { + chatContainer.scrollTop = chatContainer.scrollHeight; + } + }); + } + } + + if (data.is_connect !== undefined) { + vueInstance.isConnected = data.is_connect; + } + + if (data.remote_audio_connect !== undefined) { + vueInstance.remoteAudioConnected = data.remote_audio_connect; + } + } +} + +new Vue({ + el: '#app', + delimiters: ["[[", "]]"], + data() { + return { + messages: [], + newMessage: '', + fayService: null, + liveState: 0, + isConnected: false, + remoteAudioConnected: false, + userList: [], + selectedUser: null, + loading: false, + chatMessages: {}, + panelMsg: '', + panelReply: '', + robot:'static/images/Normal.gif', + base_url: window.location.protocol + '//' + window.location.hostname + ':' + window.location.port, + hostname: window.location.hostname, + play_sound_enabled: false, + source_record_enabled: false, + userListTimer: null, + thinkPanelExpanded: true, + thinkContent: '', + isThinkPanelMinimized: false, + mcpOnlineStatus: false, + mcpCheckTimer: null, + }; + }, + watch: { + // 消息列表变化时的监听(保留用于其他扩展) + }, + created() { + this.initFayService(); + this.getData(); + this.startUserListTimer(); + this.checkMcpStatus(); + this.startMcpStatusTimer(); + }, + methods: { + initFayService() { + const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsHost = window.location.hostname; + const wsUrl = `${wsProtocol}//${wsHost}:10003`; + this.fayService = new FayInterface(wsUrl, this.base_url, this); + this.fayService.connectWebSocket(); + this.fayService.websocket.addEventListener('open', () => { + this.loadUserList(); + }); + }, + async loadUserList() { + try { + const result = await this.fayService.getUserList(); + if (result && result.list) { + this.userList = result.list; + if (this.userList.length > 0) { + this.selectedUser = this.userList[0]; + await this.loadMessageHistory(this.selectedUser[1]); + } + } + } catch (error) { + console.error('Failed to load user list:', error); + this.$message.error('Failed to load user list. Please try again.'); + } + }, + sendMessage() { + let _this = this; + let text = _this.newMessage; + if (!text) { + alert('请输入内容'); + return; + } + if (_this.selectedUser === 'others' && !_this.othersUser) { + alert('请输入自定义用户名'); + return; + } + if (this.liveState != 1) { + alert('请先开启服务'); + return; + } + let usernameToSend = _this.selectedUser === 'others' ? _this.othersUser : _this.selectedUser[1]; + + this.timer = setTimeout(() => { + let height = document.querySelector('.chatmessage').scrollHeight; + document.querySelector('.chatmessage').scrollTop = height; + }, 1000); + _this.newMessage = ''; + let url = `${this.base_url}/api/send`; + let send_data = { + "msg": text, + "username": usernameToSend + }; + + let xhr = new XMLHttpRequest(); + xhr.open("post", url); + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xhr.send('data=' + encodeURIComponent(JSON.stringify(send_data))); + let executed = false; + xhr.onreadystatechange = async function () { + if (!executed && xhr.status === 200) { + executed = true; + } + }; + }, + getData() { + this.fayService.getRunStatus().then((data) => { + if (data) { + if(data.status){ + this.liveState = 1; + this.configEditable = false; + }else{ + this.liveState = 0; + this.configEditable = true; + } + + } + }); + this.fayService.getData().then((data) => { + if (data) { + this.updateConfigFromData(data.config); + } + }); + }, + updateConfigFromData(config) { + + if (config.interact) { + this.play_sound_enabled = config.interact.playSound; + } + if (config.source && config.source.record) { + this.source_record_enabled = config.source.record.enabled; + } + }, + saveConfig() { + let url = `${this.base_url}/api/submit`; + let send_data = { + "config": { + "source": { + "record": { + "enabled": this.source_record_enabled, + }, + }, + "interact": { + "playSound": this.play_sound_enabled, + } + } + }; + + let xhr = new XMLHttpRequest() + xhr.open("post", url) + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded") + xhr.send('data=' + JSON.stringify(send_data)) + let executed = false + xhr.onreadystatechange = async function () { + if (!executed && xhr.status === 200) { + try { + let data = await eval('(' + xhr.responseText + ')') + executed = true + } catch (e) { + } + } + } +}, + changeRecord(){ + if(this.source_record_enabled){ + this.source_record_enabled = false + }else{ + this.source_record_enabled = true + } + this.saveConfig() + }, + changeSound(){ + if(this.play_sound_enabled){ + this.play_sound_enabled = false + }else{ + this.play_sound_enabled = true + } + this.saveConfig() + }, + loadUserList() { + this.fayService.getUserList().then((response) => { + if (response && response.list) { + if (response.list.length == 0){ + // 检查是否已经有默认用户 + const defaultUserExists = this.userList.some(user => user[1] === 'User'); + if (!defaultUserExists) { + // 只有在不存在默认用户时才添加 + const info = []; + info[0] = 1; + info[1] = 'User'; + this.userList.push(info); + this.selectUser(info); + console.log('添加默认用户: User'); + } + } else { + this.userList = response.list; + if (!this.selectedUser) { + this.selectUser(this.userList[0]); + } + } + } + }); + }, + startUserListTimer() { + // 清除可能存在的旧定时器 + if (this.userListTimer) { + clearInterval(this.userListTimer); + } + // 设置新的定时器,每30秒执行一次 + this.userListTimer = setInterval(() => { + this.loadUserList(); + }, 30000); + }, + // 组件销毁时清除定时器 + beforeDestroy() { + if (this.userListTimer) { + clearInterval(this.userListTimer); + this.userListTimer = null; + } + if (this.mcpCheckTimer) { + clearInterval(this.mcpCheckTimer); + this.mcpCheckTimer = null; + } + }, + selectUser(user) { + this.selectedUser = user; + this.fayService.websocket.send(JSON.stringify({ "Username": user[1] })); + this.loadMessageHistory(user[1], 'common'); + }, + startLive() { + this.liveState = 2 + this.fayService.startLive().then(() => { + this.sendSuccessMsg('已开启!'); + this.getData(); + }); + }, + stopLive() { + this.fayService.stopLive().then(() => { + this.liveState = 3 + this.sendSuccessMsg('已关闭!'); + }); + }, + + loadMessageHistory(username, type) { + this.fayService.getMessageHistory(username).then((response) => { + if (response) { + this.messages = response; + if(type == 'common'){ + this.$nextTick(() => { + const chatContainer = document.querySelector('.chatmessage'); + if (chatContainer) { + chatContainer.scrollTop = chatContainer.scrollHeight; + } + }); + } + } + }); + }, + sendSuccessMsg(message) { + this.$notify({ + title: '成功', + message, + type: 'success', + }); + +} , +adoptText(id) { +// 调用采纳接口 +this.fayService.fetchData(`${this.base_url}/api/adopt-msg`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ id }) +}) +.then((response) => { + if (response && response.status === 'success') { + // 处理成功的响应 + this.$notify({ + title: '成功', + message: response.msg, // 显示成功消息 + type: 'success', + }); + + this.loadMessageHistory(this.selectedUser[1], 'adopt'); + } else { + // 处理失败的响应 + this.$notify({ + title: '失败', + message: response ? response.msg : '请求失败', + type: 'error', + }); + } +}) +.catch((error) => { + // 处理网络错误或HTTP错误 + this.$notify({ + title: '错误', + message: error.message || '请求失败', + type: 'error', + }); +}); +} +, + minimizeThinkPanel() { + this.isThinkPanelMinimized = !this.isThinkPanelMinimized; + const panel = document.querySelector('.think-panel'); + panel.classList.toggle('minimized'); + }, + + // 解析消息中的 think 内容 + parseThinkContent(content) { + if (!content) { + return { thinkContent: '', mainContent: '' }; + } + + // 先尝试匹配完整的 think 标签 + const completeRegex = /([\s\S]*?)<\/think>/i; + const completeMatch = content.match(completeRegex); + + if (completeMatch && completeMatch[1]) { + // 完整的 think 标签 + const rawThink = completeMatch[1]; + const thinkContent = this.trimThinkLines(rawThink); + const mainContent = content.replace(completeRegex, '').replace(/^\s+/, '').replace(/\s+$/, ''); + return { thinkContent, mainContent }; + } + + // 尝试匹配未完成的 think 标签(只有开始标签) + const incompleteRegex = /([\s\S]*)/i; + const incompleteMatch = content.match(incompleteRegex); + + if (incompleteMatch && incompleteMatch[1]) { + // 未完成的 think 标签,正在接收中 + const rawThink = incompleteMatch[1]; + const thinkContent = this.trimThinkLines(rawThink); + const mainContent = ''; // 正在思考中,主内容为空 + return { thinkContent, mainContent }; + } + + return { thinkContent: '', mainContent: content.replace(/^\s+/, '').replace(/\s+$/, '') }; + }, + + // 处理 think 内容的每行 trim + trimThinkLines(rawThink) { + const lines = rawThink.split(/\r?\n/); + const trimmedLines = []; + for (let i = 0; i < lines.length; i++) { + const trimmed = lines[i].replace(/^\s+/, '').replace(/\s+$/, ''); + if (trimmed.length > 0) { + trimmedLines.push(trimmed); + } + } + return trimmedLines.join('\n'); + }, + + // 切换 think 内容的展开/折叠状态 + toggleThink(index) { + const message = this.messages[index]; + this.$set(message, 'thinkExpanded', !message.thinkExpanded); + }, + + // 检查MCP服务器状态 + checkMcpStatus() { + const mcpUrl = `http://${this.hostname}:5010/api/mcp/servers`; + + // 使用超时设置的fetch请求 + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 3000); // 3秒超时 + + fetch(mcpUrl, { signal: controller.signal }) + .then(response => { + clearTimeout(timeoutId); + if (!response.ok) { + throw new Error('MCP服务器响应不正常'); + } + return response.json(); + }) + .then(data => { + if (Array.isArray(data)) { + // 检查是否有任何一个MCP服务器在线 + const hasOnlineServer = data.some(server => server.status === 'online'); + this.mcpOnlineStatus = hasOnlineServer; + } else { + console.warn('MCP服务器返回的数据格式不正确'); + this.mcpOnlineStatus = false; + } + }) + .catch(error => { + clearTimeout(timeoutId); + // 如果是超时错误,不输出详细错误信息 + if (error.name === 'AbortError') { + console.warn('MCP服务器请求超时'); + } else { + console.warn('检查MCP状态出错:', error.message); + } + this.mcpOnlineStatus = false; + }); + }, + + // 启动MCP状态检查定时器 + startMcpStatusTimer() { + // 清除可能存在的旧定时器 + if (this.mcpCheckTimer) { + clearInterval(this.mcpCheckTimer); + } + // 设置新的定时器,每30秒检查一次MCP状态 + this.mcpCheckTimer = setInterval(() => { + this.checkMcpStatus(); + }, 30000); + }, + + + } +}); diff --git a/gui/templates/index.html b/gui/templates/index.html index e12a750..5c902bf 100644 --- a/gui/templates/index.html +++ b/gui/templates/index.html @@ -1,131 +1,139 @@ - - - - - - - Fay数字人 - - - - - - - - - -
-
- - -
- -
- -
- -
-
- -
-
- 消息:[[panelMsg]] -
- -
-
-
-
- 接收者头像 -
-
[[item.content]]
-
- [[item.timetext]] -
- 采纳图标 -
-
- 采纳图标 -
-
-
-
-
-
-
[[item.content]]
-
[[item.timetext]]
-
- 发送者头像 -
-
-
-
- -
-
- - -
-
- -
-
- 发送信息 -
- -
- - -
-
- -
-
- -
- - - -
-
- - -
-
-
-

思考内容

- -
-

[[thinkContent]]

-
-
- -
- - - - + + + + + + + Fay数字人 + + + + + + + + + +
+
+ + +
+ +
+ +
+ +
+
+ +
+
+ 消息:[[panelMsg]] +
+ +
+
+
+
+ 接收者头像 +
+
+
+ + 思考过程 + +
+
[[parseThinkContent(item.content).thinkContent]]
+
[[parseThinkContent(item.content).mainContent]]
+
+
+ [[item.timetext]] +
+ 采纳图标 +
+
+ 采纳图标 +
+
+
+
+
+
+
[[item.content]]
+
[[item.timetext]]
+
+ 发送者头像 +
+
+
+
+ +
+
+ + +
+
+ +
+
+ 发送信息 +
+ +
+ + +
+
+ +
+
+ +
+ + + +
+
+ + +
+
+
+

思考内容

+ +
+

[[thinkContent]]

+
+
+ +
+ + + +