1267 lines
41 KiB
HTML
Executable File
1267 lines
41 KiB
HTML
Executable File
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>AI Chatbot Interface</title>
|
||
<script src="{{ url_for('static', filename='js/CubismSdkForWeb/Core/live2dcubismcore.min.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/live2d/webgl/Live2D/lib/live2d.min.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/pixi/dist/pixi.min.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/pixi-live2d-display/dist/index.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/pixi-live2d-display/dist/extra.min.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/socketio/socket.io.min.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/marked.min.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/i18n/jquery-3.7.1.min.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/i18n/jquery.i18n.min.js') }}"></script>
|
||
<script src="{{ url_for('static', filename='js/i18n/setting-lang.js') }}"></script>
|
||
|
||
<style>
|
||
* {
|
||
-webkit-user-select: none;
|
||
/* Chrome, Opera, Safari */
|
||
-moz-user-select: none;
|
||
/* Firefox */
|
||
-ms-user-select: none;
|
||
/* Internet Explorer/Edge */
|
||
user-select: none;
|
||
/* Non-prefixed version, currently supported by Chrome, Opera and Firefox */
|
||
-webkit-tap-highlight-color: transparent;
|
||
/* 禁用触控点击的蓝色叠加层 */
|
||
}
|
||
body,
|
||
html {
|
||
height: 100%;
|
||
margin: 0;
|
||
font-family: Arial, sans-serif;
|
||
background-color: #ffffff00;
|
||
}
|
||
.chat-container {
|
||
/* max-width: 360px; */
|
||
height: 100%;
|
||
margin: 0 auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
/* background-color: rgba(255, 255, 255, 0); */
|
||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||
/* background: linear-gradient(to top, rgba(255, 255, 255, 0.5) 30%, rgba(255,255,255,0) 100%) */
|
||
}
|
||
|
||
#canvas {
|
||
height: 100%;
|
||
width: 100%;
|
||
position: absolute;
|
||
background-color: transparent;
|
||
z-index: 0;
|
||
}
|
||
|
||
.chat-header {
|
||
padding: 15px;
|
||
background-color: white;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.back-arrow {
|
||
color: #8e8e8e;
|
||
font-size: 24px;
|
||
margin-right: 15px;
|
||
}
|
||
.chat-title {
|
||
color: #8e8e8e;
|
||
font-size: 16px;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.chat-messages {
|
||
bottom: 110px;
|
||
overflow-y: auto;
|
||
position: relative;
|
||
padding: 20px 15px;
|
||
/* background: linear-gradient(to top, rgb(247,242,242,0.43) 85%, rgba(247,242,242,0) 100%); */
|
||
margin-top: 50vh;
|
||
mask-image: linear-gradient(
|
||
to top,
|
||
rgba(0, 0, 0, 1) 75%,
|
||
rgba(0, 0, 0, 0) 100%
|
||
);
|
||
/* mask: url('#mask-gradient') bottom / cover; */
|
||
-webkit-mask: linear-gradient(
|
||
to top,
|
||
rgba(0, 0, 0, 1) 75%,
|
||
rgba(0, 0, 0, 0) 100%
|
||
);
|
||
/* -webkit-mask-image: url('#mask-gradient') bottom / cover; */
|
||
/* mask-image: url('../static/images/gradient-mask_svg.svg'); */
|
||
/* mask-repeat: no-repeat; */
|
||
/* mask-size: cover; */
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 隐藏滚动条 */
|
||
.chat-messages::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
|
||
.chat-messages {
|
||
-ms-overflow-style: none; /* IE and Edge */
|
||
scrollbar-width: none; /* Firefox */
|
||
}
|
||
|
||
.message {
|
||
padding: 20px 15px;
|
||
border-radius: 18px;
|
||
max-width: 70%;
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
animation: fadeIn 0.3s;
|
||
user-select: none;
|
||
}
|
||
.message:not(:last-child) {
|
||
margin-bottom: 10px;
|
||
}
|
||
.user-message {
|
||
background-color: #dcf8c6b7;
|
||
align-self: flex-end;
|
||
margin-left: auto;
|
||
}
|
||
.ai-message {
|
||
position: relative;
|
||
padding: 15px;
|
||
border-radius: 10px;
|
||
background-color: #e2e9fd;
|
||
margin-bottom: 10px;
|
||
line-height: 1.6;
|
||
font-size: 14px;
|
||
}
|
||
.ai-message pre {
|
||
background: rgba(255, 255, 255, 0.5);
|
||
padding: 1em;
|
||
border-radius: 8px;
|
||
overflow-x: auto;
|
||
margin: 1em 0;
|
||
}
|
||
.ai-message code {
|
||
background: rgba(255, 255, 255, 0.5);
|
||
padding: 0.2em 0.4em;
|
||
border-radius: 4px;
|
||
font-family: "Courier New", Courier, monospace;
|
||
font-size: 0.9em;
|
||
}
|
||
.ai-message pre code {
|
||
background: none;
|
||
padding: 0;
|
||
}
|
||
.ai-message blockquote {
|
||
border-left: 4px solid #914ac5;
|
||
margin: 1em 0;
|
||
padding-left: 1em;
|
||
color: #666;
|
||
}
|
||
.ai-message table {
|
||
border-collapse: collapse;
|
||
width: 100%;
|
||
margin: 1em 0;
|
||
}
|
||
.ai-message th,
|
||
.ai-message td {
|
||
border: 1px solid rgba(145, 74, 197, 0.2);
|
||
padding: 0.5em;
|
||
}
|
||
.ai-message th {
|
||
background: rgba(255, 255, 255, 0.5);
|
||
color: #914ac5;
|
||
}
|
||
.typing-cursor {
|
||
display: inline-block;
|
||
width: 2px;
|
||
height: 1.2em;
|
||
background: #914ac5;
|
||
margin-left: 2px;
|
||
vertical-align: middle;
|
||
animation: blink 0.75s step-end infinite;
|
||
}
|
||
@keyframes blink {
|
||
from,
|
||
to {
|
||
opacity: 0;
|
||
}
|
||
50% {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
.reasoning-message {
|
||
color: gray;
|
||
font-size: 12px; /* Smaller font size */
|
||
display: block; /* Show by default */
|
||
margin: 6px;
|
||
background-color: #ffffff73; /* Light gray background */
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
box-sizing: border-box;
|
||
}
|
||
.reasoning-message-title {
|
||
font-size: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 4px;
|
||
}
|
||
.reasoning-icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
margin-right: 5px;
|
||
}
|
||
.chatbot-btn-container {
|
||
position: fixed;
|
||
bottom: 65px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
height: 32px;
|
||
width: 100%;
|
||
padding: 0 10px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
#deep-toggle-btn,
|
||
#search-toggle-btn {
|
||
border-radius: 14px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 32px;
|
||
padding: 0 14px;
|
||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||
font-size: 10px;
|
||
background-color: #f5f5f5;
|
||
color: #999999;
|
||
}
|
||
#deep-toggle-icon,
|
||
#search-toggle-icon {
|
||
width: 16px;
|
||
height: 16px;
|
||
margin-right: 6px;
|
||
}
|
||
#deep-toggle-btn #deep-dot,
|
||
#search-toggle-btn #search-dot {
|
||
margin-left: 6px;
|
||
width: 5px;
|
||
height: 5px;
|
||
border-radius: 50%;
|
||
background-color: #999999;
|
||
}
|
||
#deep-toggle-btn.active {
|
||
background-color: #c9d8f7;
|
||
color: #4d6bfe;
|
||
border-color: rgba(0, 122, 255, 0.15);
|
||
}
|
||
#deep-toggle-btn.active #deep-dot,
|
||
#search-toggle-btn.active #search-dot {
|
||
background-color: #ff8401;
|
||
}
|
||
|
||
#deep-toggle-btn {
|
||
margin-right: 10px;
|
||
}
|
||
#search-toggle-btn.active {
|
||
background-color: #c9d8f7;
|
||
color: #4d6bfe;
|
||
border-color: rgba(0, 122, 255, 0.15);
|
||
}
|
||
.chat-input-background {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
/* background-color: #f7f2f26e; */
|
||
/* padding: 10px; */
|
||
/* border-top: 1px solid #e0e0e0; */
|
||
z-index: 4;
|
||
}
|
||
.chat-input {
|
||
display: flex;
|
||
background-color: white; /* 保持原有背景颜色 */
|
||
border-radius: 15px; /* 确保内部元素的圆角与外部容器相匹配 */
|
||
overflow: hidden;
|
||
z-index: 2;
|
||
height: 55px;
|
||
align-items: center;
|
||
box-sizing: border-box;
|
||
padding: 0 10px;
|
||
}
|
||
#message-input,
|
||
#voice-input-button {
|
||
flex: 1;
|
||
/* padding: 10px; */
|
||
border: none;
|
||
color: #333;
|
||
border-radius: 8px;
|
||
outline: none;
|
||
}
|
||
#message-input {
|
||
display: none;
|
||
font-size: 14px;
|
||
/* 文字可以选中 */
|
||
user-select: text;
|
||
}
|
||
#message-input::placeholder {
|
||
color: #999;
|
||
}
|
||
#voice-input-button {
|
||
display: block;
|
||
font-size: 12px;
|
||
height: 36px;
|
||
/* 文字不能选中 */
|
||
user-select: none;
|
||
transition: background-color 0.2s, box-shadow 0.2s; /* 平滑过渡 */
|
||
}
|
||
.chat-change-button {
|
||
background: none;
|
||
padding: 0 10px;
|
||
}
|
||
.chat-change-button,
|
||
.send-button {
|
||
border: none;
|
||
color: #999;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
border-radius: 8px;
|
||
}
|
||
.send-button {
|
||
display: none;
|
||
padding: 0 15px;
|
||
height: 36px;
|
||
}
|
||
|
||
#voice-input-button:active,
|
||
.send-button:active {
|
||
background-color: #dcdcdc; /* 按下时颜色稍微变深 */
|
||
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.chat-change-button img,
|
||
.send-button img {
|
||
opacity: 0.4;
|
||
}
|
||
|
||
.chat-change-button img {
|
||
height: 25px;
|
||
width: 25px;
|
||
}
|
||
.send-button img {
|
||
width: 22px;
|
||
height: 22px;
|
||
}
|
||
@keyframes fadeIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
#chat-modal {
|
||
position: fixed;
|
||
display: none;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
z-index: 3;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
/* 动画:发散的红色光线内阴影 */
|
||
@keyframes Glow {
|
||
0% {
|
||
box-shadow: inset 0 0 20px 0 rgba(42, 202, 250, 0.5); /* 初始小光线 */
|
||
}
|
||
100% {
|
||
box-shadow: inset 0 0 50px 25px rgba(42, 202, 250, 0.8); /* 最大光线 */
|
||
}
|
||
}
|
||
|
||
/* 启动红色发散的内阴影效果 */
|
||
#chat-modal.glow {
|
||
animation: Glow 1s forwards;
|
||
}
|
||
#chat-modal.cancel-glow {
|
||
box-shadow: inset 0 0 50px 25px rgba(255, 255, 255, 0.8) !important;
|
||
}
|
||
|
||
#hint-text {
|
||
position: absolute;
|
||
display: none;
|
||
padding: 4px 10px;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
color: #fff;
|
||
font-size: 13px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 5px;
|
||
bottom: 75px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 4;
|
||
}
|
||
|
||
#recording {
|
||
z-index: 4;
|
||
position: absolute;
|
||
display: none;
|
||
width: 160px;
|
||
bottom: 120px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
}
|
||
|
||
#recording-img {
|
||
width: 100%;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<svg style="position: absolute; width: 0; height: 0">
|
||
<defs>
|
||
<linearGradient id="gradient-mask" x1="0%" y1="100%" x2="0%" y2="0%">
|
||
<stop offset="75%" style="stop-color: white; stop-opacity: 1" />
|
||
<stop offset="100%" style="stop-color: white; stop-opacity: 0" />
|
||
</linearGradient>
|
||
<mask
|
||
id="mask-gradient"
|
||
maskUnits="objectBoundingBox"
|
||
maskContentUnits="objectBoundingBox"
|
||
>
|
||
<rect x="0" y="0" width="1" height="1" fill="url(#gradient-mask)" />
|
||
</mask>
|
||
</defs>
|
||
</svg>
|
||
<canvas id="canvas"></canvas>
|
||
<div class="chat-container">
|
||
<div id="chat-modal"></div>
|
||
<div id="hint-text" i18n="control.chatbot.recording"></div>
|
||
<div id="recording">
|
||
<img
|
||
id="recording-img"
|
||
src="/static/images/ai_chatbot/recording2.gif"
|
||
alt=""
|
||
/>
|
||
</div>
|
||
<!-- <div class="chat-header">
|
||
<span class="back-arrow">←</span>
|
||
<h2 class="chat-title">johndoe92</h2>
|
||
</div> -->
|
||
<div class="chat-messages" id="chat-messages">
|
||
<!-- <div class="message user-message">小美小美</div> -->
|
||
<!-- <div class="message ai-message">哎 我在</div> -->
|
||
<div class="message ai-message" i18n="control.chatbot.message"></div>
|
||
</div>
|
||
<div class="chatbot-btn-container">
|
||
<div class="deep-toggle-btn" id="deep-toggle-btn">
|
||
<img
|
||
id="deep-toggle-icon"
|
||
src="/static/images/ai_chatbot/no-deep.svg"
|
||
alt=""
|
||
/>
|
||
<span i18n="control.chatbot.deep"></span>
|
||
<div class="deep-dot" id="deep-dot"></div>
|
||
</div>
|
||
<div class="search-toggle-btn" id="search-toggle-btn">
|
||
<img
|
||
id="search-toggle-icon"
|
||
src="/static/images/ai_chatbot/no-aisearch.svg"
|
||
alt=""
|
||
/>
|
||
<span i18n="control.chatbot.onlineSearch"></span>
|
||
<div class="search-dot" id="search-dot"></div>
|
||
</div>
|
||
</div>
|
||
<div class="chat-input-background">
|
||
<div class="chat-input">
|
||
<button class="chat-change-button" id="chat-change-button">
|
||
<img
|
||
id="chat-change-img"
|
||
src="/static/images/ai_chatbot/keyboard.png"
|
||
alt="输入方式切换"
|
||
/>
|
||
</button>
|
||
<input type="text" id="message-input" placeholder="Type something" />
|
||
<button
|
||
class="voice-input-button"
|
||
id="voice-input-button"
|
||
i18n="control.chatbot.pressAndHoldToTalk"
|
||
></button>
|
||
<button class="send-button" id="send-button" onclick="sendMessage()">
|
||
<img
|
||
src="{{ url_for('static', filename='images/ai_chatbot/send.svg') }}"
|
||
alt="发送"
|
||
/>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="../static/js/base.js"></script>
|
||
|
||
<script>
|
||
const canvas = document.getElementById("canvas");
|
||
const app = new PIXI.Application({
|
||
view: canvas,
|
||
autoStart: true,
|
||
resizeTo: window,
|
||
transparent: true,
|
||
});
|
||
|
||
let model;
|
||
let lastX = 0;
|
||
let lastY = 0;
|
||
|
||
async function loadLive2DModel(url) {
|
||
try {
|
||
model = await PIXI.live2d.Live2DModel.from(url);
|
||
app.stage.addChild(model);
|
||
// model.x = 0;
|
||
// model.scale.set(0.25);
|
||
// const scaleX = (innerWidth * 0.4) / model.width;
|
||
// const scaleY = (innerHeight * 0.8) / model.height;
|
||
|
||
// fit the window
|
||
// model.scale.set(Math.min(scaleX, scaleY));
|
||
|
||
// model.y = innerHeight * 0.1;
|
||
// 计算模型的原始尺寸
|
||
const originalWidth = model.internalModel.width;
|
||
const originalHeight = model.internalModel.height;
|
||
|
||
// 计算缩放比例以适应窗口
|
||
const scaleX = ((window.innerWidth * 0.8) / originalWidth) * 2.5;
|
||
const scaleY = ((window.innerHeight * 0.8) / originalHeight) * 2.5;
|
||
const scale = Math.min(scaleX, scaleY);
|
||
model.scale.set(scale, scale);
|
||
|
||
// 将模型居中
|
||
model.x = (window.innerWidth - originalWidth * scale) / 2;
|
||
model.y =
|
||
(canvas.clientHeight - originalHeight * scale) / 2 +
|
||
canvas.clientHeight / 2;
|
||
addFrame(model);
|
||
addHitAreaFrames(model);
|
||
|
||
// model.internalModel.coreModel.setParameterValueById("ParamMouthOpenY", 0.65);
|
||
// model.internalModel.coreModel.setParameterValueById("ParamMouthForm", 0);
|
||
// console.log(model.internalModel.coreModel.getParameterValueById("ParamMouthOpenY"),model.internalModel.coreModel.getParameterValueById("ParamMouthForm"))
|
||
|
||
model.motion("mouth_movement", {
|
||
priority: 3, // 通常默认动作优先级为1,高优先级可以设为2或3
|
||
});
|
||
|
||
model.on("hit", (hitAreas) => {
|
||
if (hitAreas.includes("Body")) {
|
||
console.log("hit Body");
|
||
// model.motion("Tap");
|
||
}
|
||
|
||
if (hitAreas.includes("Head")) {
|
||
console.log("hit Head");
|
||
// model4.expression();
|
||
// model.motion("Idle");
|
||
// aiResponse = "嘿!管好你的手,别乱碰我!"
|
||
// addMessage('ai', aiResponse);
|
||
// socket.emit('synthesize_audio', { text: aiResponse });
|
||
}
|
||
});
|
||
|
||
console.log(
|
||
"Loaded model parameters:",
|
||
model.internalModel.coreModel.parameters
|
||
);
|
||
} catch (error) {
|
||
console.error("Failed to load Live2D model:", error);
|
||
}
|
||
}
|
||
|
||
function addFrame(model) {
|
||
const foreground = PIXI.Sprite.from(PIXI.Texture.WHITE);
|
||
foreground.width = model.internalModel.width;
|
||
foreground.height = model.internalModel.height;
|
||
foreground.alpha = 0.2;
|
||
|
||
model.addChild(foreground);
|
||
|
||
foreground.visible = false;
|
||
}
|
||
|
||
function addHitAreaFrames(model) {
|
||
const hitAreaFrames = new PIXI.live2d.HitAreaFrames();
|
||
|
||
model.addChild(hitAreaFrames);
|
||
|
||
hitAreaFrames.visible = false;
|
||
}
|
||
|
||
canvas.addEventListener("mousedown", onMouseDown);
|
||
canvas.addEventListener("touchstart", onTouchStart);
|
||
|
||
function onMouseDown(event) {
|
||
event.preventDefault();
|
||
lastX = event.clientX;
|
||
lastY = event.clientY;
|
||
updateModelLookAt(event.clientX, event.clientY);
|
||
document.addEventListener("mousemove", onMouseMove);
|
||
document.addEventListener("mouseup", onMouseUp);
|
||
}
|
||
|
||
function onTouchStart(event) {
|
||
event.preventDefault();
|
||
const touch = event.touches[0];
|
||
lastX = touch.clientX;
|
||
lastY = touch.clientY;
|
||
updateModelLookAt(touch.clientX, touch.clientY);
|
||
document.addEventListener("touchmove", onTouchMove);
|
||
document.addEventListener("touchend", onTouchEnd);
|
||
}
|
||
|
||
function onMouseMove(event) {
|
||
event.preventDefault();
|
||
updateModelLookAt(event.clientX, event.clientY);
|
||
lastX = event.clientX;
|
||
lastY = event.clientY;
|
||
}
|
||
|
||
function onTouchMove(event) {
|
||
event.preventDefault();
|
||
const touch = event.touches[0];
|
||
updateModelLookAt(touch.clientX, touch.clientY);
|
||
lastX = touch.clientX;
|
||
lastY = touch.clientY;
|
||
}
|
||
|
||
function onMouseUp() {
|
||
document.removeEventListener("mousemove", onMouseMove);
|
||
document.removeEventListener("mouseup", onMouseUp);
|
||
}
|
||
|
||
function onTouchEnd() {
|
||
document.removeEventListener("touchmove", onTouchMove);
|
||
document.removeEventListener("touchend", onTouchEnd);
|
||
}
|
||
|
||
function updateModelLookAt(currentX, currentY) {
|
||
if (model) {
|
||
const dx = currentX - lastX;
|
||
const dy = currentY - lastY;
|
||
// const angleX = model.internalModel.coreModel.getParameterValueById('PARAM_ANGLE_X');
|
||
// const angleY = model.internalModel.coreModel.getParameterValueById('PARAM_ANGLE_Y');
|
||
// model.internalModel.coreModel.setParameterValueById('PARAM_ANGLE_X', angleX + dx * 0.1);
|
||
// model.internalModel.coreModel.setParameterValueById('PARAM_ANGLE_Y', angleY - dy * 0.1);
|
||
}
|
||
}
|
||
|
||
const socket = io();
|
||
|
||
// document.getElementById('sendButton').addEventListener('click', () => {
|
||
// const text = document.getElementById('textInput').value;
|
||
// socket.emit('synthesize_audio', { text: text });
|
||
// });
|
||
function setMouthParameters(mouthType) {
|
||
let Y_value, F_value;
|
||
|
||
switch (mouthType) {
|
||
case "A":
|
||
Y_value = 0;
|
||
F_value = 1;
|
||
break;
|
||
case "B":
|
||
Y_value = 0.4;
|
||
F_value = 0.4;
|
||
break;
|
||
case "C":
|
||
Y_value = 0.65;
|
||
F_value = 0.65;
|
||
break;
|
||
case "D":
|
||
Y_value = 1;
|
||
F_value = 1;
|
||
break;
|
||
case "E":
|
||
Y_value = 0.75;
|
||
F_value = 0.35;
|
||
break;
|
||
case "F":
|
||
Y_value = 0.65;
|
||
F_value = 0;
|
||
break;
|
||
default:
|
||
console.error("Invalid mouth type provided");
|
||
return;
|
||
}
|
||
|
||
model.internalModel.coreModel.setParameterValueById(
|
||
"ParamMouthOpenY",
|
||
Y_value
|
||
);
|
||
model.internalModel.coreModel.setParameterValueById(
|
||
"ParamMouthForm",
|
||
F_value
|
||
);
|
||
}
|
||
|
||
socket.on("mouth_movement", (data) => {
|
||
if (model) {
|
||
value = Math.max(0, Math.min(1, data.value));
|
||
// model.internalModel.coreModel.setParameterValueById('PARAM_MOUTH_OPEN_Y', value);
|
||
model.internalModel.coreModel.setParameterValueById(
|
||
"ParamMouthOpenY",
|
||
value * 2.5
|
||
);
|
||
model.internalModel.coreModel.setParameterValueById(
|
||
"ParamMouthForm",
|
||
1 - value * 2
|
||
);
|
||
// value = data.value
|
||
// setMouthParameters(value)
|
||
|
||
// console.log("Mouth open value:", value);
|
||
|
||
// setTimeout(() => {
|
||
// model.internalModel.coreModel.setParameterValueById("ParamMouthOpenY", 0);
|
||
// }, 200); // 100 毫秒的延迟
|
||
}
|
||
});
|
||
|
||
const sendButton = document.getElementById("send-button");
|
||
// const modelUrl = "/live2d/Epsilon_free/runtime/Epsilon_free.model3.json";
|
||
const modelUrl = "/static/live2d/haru/haru_greeter_t03.model3.json";
|
||
// const modelUrl = "https://cdn.jsdelivr.net/gh/guansss/pixi-live2d-display/test/assets/haru/haru_greeter_t03.model3.json"
|
||
|
||
loadLive2DModel(modelUrl);
|
||
|
||
// document
|
||
// .querySelector(".send-button")
|
||
// .addEventListener("touchstart", function () {
|
||
// if (typeof AndroidInterface !== "undefined") {
|
||
// AndroidInterface.sendMessageToAndroid("Speak Button Pressed");
|
||
// }
|
||
// });
|
||
|
||
// document
|
||
// .querySelector(".send-button")
|
||
// .addEventListener("touchend", function () {
|
||
// if (typeof AndroidInterface !== "undefined") {
|
||
// AndroidInterface.sendMessageToAndroid("Speak Button Released");
|
||
// }
|
||
// });
|
||
const messageInput = document.getElementById("message-input");
|
||
|
||
function sendMessage() {
|
||
const message = messageInput.value.trim();
|
||
|
||
if (message) {
|
||
socket.emit("send_message", { message });
|
||
messageInput.value = "";
|
||
}
|
||
}
|
||
|
||
socket.on("add_message", function (data) {
|
||
const { sender, message, isReasoning } = data;
|
||
const isReasoningBool =
|
||
typeof isReasoning === "boolean" ? isReasoning : Boolean(isReasoning);
|
||
addMessage(sender, message, isReasoningBool);
|
||
});
|
||
|
||
let lastAiMessageTime = 0; // 记录上一次 AI 消息的时间
|
||
|
||
function addMessage(sender, text, isReasoning) {
|
||
const messagesContainer = document.getElementById("chat-messages");
|
||
const messageElements = Array.from(
|
||
messagesContainer.getElementsByClassName("message")
|
||
);
|
||
let messageElement;
|
||
|
||
if (sender === "ai") {
|
||
const currentTime = new Date().getTime();
|
||
const timeDifference = currentTime - lastAiMessageTime;
|
||
|
||
// 如果是 reasoning 部分,首先输出 reasoning
|
||
if (isReasoning) {
|
||
if (
|
||
timeDifference <= 5000 &&
|
||
messageElements.length > 0 &&
|
||
messageElements[messageElements.length - 1].classList.contains(
|
||
"ai-message"
|
||
) &&
|
||
messageElements[messageElements.length - 1].querySelector(
|
||
".reasoning-message"
|
||
)
|
||
) {
|
||
// 如果上次消息是 reasoning 部分且时间差不超过 5 秒,拼接到上一条 reasoning 消息
|
||
const reasoningContent =
|
||
messageElements[messageElements.length - 1].querySelector(
|
||
".reasoning-message"
|
||
);
|
||
const currentText =
|
||
reasoningContent.getAttribute("data-raw-text") || "";
|
||
const newText = currentText + text;
|
||
reasoningContent.setAttribute("data-raw-text", newText);
|
||
reasoningContent.innerHTML = `
|
||
<div class='reasoning-message-title'>
|
||
<img src='/static/images/ai_chatbot/深度思考.svg' class='reasoning-icon' alt='Reasoning Icon' />
|
||
<span>深度思考</span>
|
||
</div>
|
||
<br>
|
||
${marked.parse(newText)}
|
||
`;
|
||
// 高亮代码块
|
||
reasoningContent.querySelectorAll("pre code").forEach((block) => {
|
||
hljs.highlightBlock(block);
|
||
});
|
||
} else {
|
||
// 创建新的 reasoning 消息框
|
||
messageElement = document.createElement("div");
|
||
messageElement.classList.add("message", "ai-message");
|
||
|
||
const reasoningContent = document.createElement("div");
|
||
reasoningContent.className = "reasoning-message";
|
||
reasoningContent.setAttribute("data-raw-text", text);
|
||
reasoningContent.innerHTML = `
|
||
<div class='reasoning-message-title'>
|
||
<img src='/static/images/ai_chatbot/深度思考.svg' class='reasoning-icon' alt='Reasoning Icon' />
|
||
<span>深度思考</span>
|
||
</div>
|
||
<br>
|
||
${marked.parse(text)}
|
||
`;
|
||
|
||
messageElement.appendChild(reasoningContent);
|
||
messagesContainer.appendChild(messageElement);
|
||
|
||
// 高亮代码块
|
||
reasoningContent.querySelectorAll("pre code").forEach((block) => {
|
||
hljs.highlightBlock(block);
|
||
});
|
||
}
|
||
} else {
|
||
// 非-reasoning 部分
|
||
if (
|
||
timeDifference <= 5000 &&
|
||
messageElements.length > 0 &&
|
||
messageElements[messageElements.length - 1].classList.contains(
|
||
"ai-message"
|
||
) &&
|
||
!messageElements[messageElements.length - 1].querySelector(
|
||
".reasoning-message"
|
||
)
|
||
) {
|
||
// 如果上次消息是普通 AI 消息且时间差不超过 5 秒,合并消息
|
||
const lastMessage = messageElements[messageElements.length - 1];
|
||
const currentText =
|
||
lastMessage.getAttribute("data-raw-text") || "";
|
||
const newText = currentText + text;
|
||
lastMessage.setAttribute("data-raw-text", newText);
|
||
lastMessage.innerHTML = marked.parse(newText);
|
||
|
||
// 高亮代码块
|
||
lastMessage.querySelectorAll("pre code").forEach((block) => {
|
||
hljs.highlightBlock(block);
|
||
});
|
||
} else {
|
||
// 创建新的消息框
|
||
messageElement = document.createElement("div");
|
||
messageElement.classList.add("message", "ai-message");
|
||
messageElement.setAttribute("data-raw-text", text);
|
||
messageElement.innerHTML = marked.parse(text);
|
||
|
||
messagesContainer.appendChild(messageElement);
|
||
|
||
// 高亮代码块
|
||
messageElement.querySelectorAll("pre code").forEach((block) => {
|
||
hljs.highlightBlock(block);
|
||
});
|
||
}
|
||
}
|
||
|
||
lastAiMessageTime = currentTime;
|
||
} else if (sender === "new-ai") {
|
||
const { reasoning_content, content } = text;
|
||
if (reasoning_content && content) {
|
||
messageElement = document.createElement("div");
|
||
messageElement.classList.add("message", "ai-message");
|
||
|
||
const reasoningDiv = document.createElement("div");
|
||
reasoningDiv.className = "reasoning-message";
|
||
reasoningDiv.setAttribute("data-raw-text", reasoning_content);
|
||
reasoningDiv.innerHTML = `
|
||
<div class='reasoning-message-title'>
|
||
<img src='/static/images/ai_chatbot/深度思考.svg' class='reasoning-icon' alt='Reasoning Icon' />
|
||
<span>深度思考</span>
|
||
</div>
|
||
<br>
|
||
${marked.parse(reasoning_content)}
|
||
`;
|
||
|
||
const contentDiv = document.createElement("div");
|
||
contentDiv.setAttribute("data-raw-text", content);
|
||
contentDiv.innerHTML = marked.parse(content);
|
||
|
||
messageElement.appendChild(reasoningDiv);
|
||
messageElement.appendChild(contentDiv);
|
||
|
||
messagesContainer.appendChild(messageElement);
|
||
|
||
// 高亮所有代码块
|
||
messageElement.querySelectorAll("pre code").forEach((block) => {
|
||
hljs.highlightBlock(block);
|
||
});
|
||
} else if (content) {
|
||
messageElement = document.createElement("div");
|
||
messageElement.classList.add("message", "ai-message");
|
||
messageElement.setAttribute("data-raw-text", content);
|
||
messageElement.innerHTML = marked.parse(content);
|
||
|
||
messagesContainer.appendChild(messageElement);
|
||
|
||
// 高亮代码块
|
||
messageElement.querySelectorAll("pre code").forEach((block) => {
|
||
hljs.highlightBlock(block);
|
||
});
|
||
}
|
||
} else {
|
||
// 用户消息
|
||
messageElement = document.createElement("div");
|
||
messageElement.classList.add("message", "user-message");
|
||
messageElement.textContent = text;
|
||
|
||
messagesContainer.appendChild(messageElement);
|
||
}
|
||
|
||
// 滚动到最底部
|
||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||
}
|
||
|
||
document
|
||
.getElementById("message-input")
|
||
.addEventListener("keypress", function (e) {
|
||
if (e.key === "Enter") {
|
||
sendMessage();
|
||
}
|
||
});
|
||
|
||
fetch("/get_history")
|
||
.then((response) => response.json())
|
||
.then((data) => {
|
||
data.forEach((element) => {
|
||
const { sender, message } = element;
|
||
if (sender === "ai") {
|
||
addMessage("new-ai", message);
|
||
} else {
|
||
addMessage("user", message);
|
||
}
|
||
});
|
||
})
|
||
.catch((error) => console.error("Error fetching chat list:", error));
|
||
|
||
const chatChangeButton = document.getElementById("chat-change-button");
|
||
const chatChangeImg = document.getElementById("chat-change-img");
|
||
const voiceInputButton = document.getElementById("voice-input-button");
|
||
|
||
chatChangeButton.addEventListener("click", function () {
|
||
if (chatChangeImg.src.includes("voice.png")) {
|
||
setInputDisplay("voice");
|
||
} else if (chatChangeImg.src.includes("keyboard.png")) {
|
||
setInputDisplay("keyboard");
|
||
}
|
||
// if (typeof AndroidInterface !== "undefined") {
|
||
// AndroidInterface.startSpeak();
|
||
// }
|
||
});
|
||
|
||
function setInputDisplay(mode) {
|
||
chatChangeImg.src =
|
||
mode === "voice"
|
||
? "/static/images/ai_chatbot/keyboard.png"
|
||
: "/static/images/ai_chatbot/voice.png";
|
||
voiceInputButton.style.display = mode === "voice" ? "block" : "none";
|
||
messageInput.style.display = mode === "voice" ? "none" : "block";
|
||
sendButton.style.display = mode === "voice" ? "none" : "flex";
|
||
}
|
||
|
||
const chatModal = document.getElementById("chat-modal");
|
||
const hintText = document.getElementById("hint-text");
|
||
const recordingBox = document.getElementById("recording");
|
||
const recordingImg = document.getElementById("recording-img");
|
||
|
||
let pressTimer;
|
||
let isLongPress = false;
|
||
let startY = 0;
|
||
let startX = 0; // 记录起始位置
|
||
let isSlidingUp = false;
|
||
let isSlidingDown = false; // 用于判断是否发生了滑动回原位置
|
||
|
||
voiceInputButton.addEventListener("touchstart", function (event) {
|
||
// 开始录音
|
||
handleAudioControl("start");
|
||
|
||
voiceInputButton.innerText = "松开结束录音";
|
||
const touch = event.touches[0]; // 获取第一个触摸点
|
||
chatModal.style.display = "block";
|
||
setHintDisplay("flex");
|
||
setHintText("录音中...上滑可取消录音");
|
||
chatModal.classList.add("glow");
|
||
|
||
// 记录按下的时间
|
||
pressTimer = setTimeout(() => {
|
||
isLongPress = true;
|
||
}, 600); // 600ms 后被认为是长按
|
||
|
||
// 记录按下的起始位置
|
||
startY = touch.clientY;
|
||
startX = touch.clientX;
|
||
|
||
// 初始化滑动状态
|
||
isSlidingUp = false;
|
||
isSlidingDown = false;
|
||
});
|
||
|
||
voiceInputButton.addEventListener("touchend", function (event) {
|
||
// 清除定时器
|
||
clearTimeout(pressTimer);
|
||
chatModal.style.display = "none";
|
||
|
||
if (isLongPress) {
|
||
if (isSlidingUp) {
|
||
setHintText("取消录音");
|
||
// 取消录音逻辑
|
||
handleAudioControl("cancel");
|
||
} else {
|
||
// 发送录音逻辑
|
||
handleAudioControl("stop");
|
||
}
|
||
} else {
|
||
setHintText("录音时间太短");
|
||
// 取消录音逻辑
|
||
handleAudioControl("cancel"); // 当按钮取消时
|
||
}
|
||
|
||
setTimeout(() => {
|
||
setHintDisplay("none");
|
||
voiceInputButton.innerText = "按住说话";
|
||
}, 300);
|
||
chatModal.classList.remove("glow");
|
||
chatModal.classList.remove("cancel-glow");
|
||
// 重置状态
|
||
isLongPress = false;
|
||
startY = 0;
|
||
startX = 0;
|
||
});
|
||
|
||
voiceInputButton.addEventListener("touchmove", function (event) {
|
||
if (isLongPress) {
|
||
const touch = event.touches[0]; // 获取第一个触摸点
|
||
const currentY = touch.clientY;
|
||
|
||
// 判断是否发生了上滑
|
||
if (currentY < startY - 30 && !isSlidingUp && !isSlidingDown) {
|
||
isSlidingUp = true; // 标记为上滑
|
||
isSlidingDown = false; // 取消回滑状态
|
||
setHintText("松开取消录音,移动回原位置继续录音");
|
||
// 添加的边框效果
|
||
chatModal.classList.add("cancel-glow");
|
||
recordingImg.src = "/static/images/ai_chatbot/recording2.gif";
|
||
}
|
||
|
||
// 判断是否发生了下滑回原位置
|
||
if (currentY > startY && isSlidingUp) {
|
||
isSlidingDown = true; // 发生了回滑
|
||
isSlidingUp = false; // 取消上滑状态
|
||
setHintText("录音中...上滑可取消录音");
|
||
chatModal.classList.remove("cancel-glow");
|
||
recordingImg.src = "/static/images/ai_chatbot/recording.gif";
|
||
}
|
||
|
||
// 如果回到原位后继续上滑,恢复上滑状态
|
||
if (currentY < startY - 30 && isSlidingDown) {
|
||
isSlidingUp = true; // 恢复上滑状态
|
||
isSlidingDown = false; // 取消回滑状态
|
||
setHintText("松开取消录音,移动回原位置继续录音");
|
||
chatModal.classList.add("cancel-glow");
|
||
recordingImg.src = "/static/images/ai_chatbot/recording2.gif";
|
||
}
|
||
}
|
||
});
|
||
function setHintText(text) {
|
||
hintText.innerText = text;
|
||
}
|
||
function setHintDisplay(display) {
|
||
recordingBox.style.display = display;
|
||
hintText.style.display = display;
|
||
recordingImg.src = "/static/images/ai_chatbot/recording.gif";
|
||
}
|
||
|
||
function handleAudioControl(action) {
|
||
// 发送消息到 Android
|
||
if (typeof AndroidInterface !== "undefined") {
|
||
if (action === "start") {
|
||
AndroidInterface.sendMessageToAndroid("Speak Button Pressed");
|
||
} else if (action === "cancel") {
|
||
AndroidInterface.sendMessageToAndroid("Speak Button Cancelled");
|
||
} else if (action === "stop") {
|
||
AndroidInterface.sendMessageToAndroid("Speak Button Released");
|
||
}
|
||
}
|
||
|
||
// 只有当 action 不是 "stop" 时,才发送控制命令到后端
|
||
if (action !== "stop") {
|
||
socket.emit("speech_audio_control", action);
|
||
}
|
||
}
|
||
|
||
const deepToggleBtn = document.querySelector("#deep-toggle-btn");
|
||
const deepToggleIcon = document.querySelector("#deep-toggle-icon");
|
||
const deepToggleDot = document.querySelector("#deep-toggle-dot");
|
||
|
||
let deepStatus = false;
|
||
|
||
// 获取深度思考状态
|
||
function getDeepThoughtStatus() {
|
||
fetch("/get_deep_thought", {
|
||
method: "GET",
|
||
})
|
||
.then((response) => response.json())
|
||
.then((data) => {
|
||
if (data.status === "success") {
|
||
console.log(
|
||
"Current deep thought status:",
|
||
data.deep_thought_active
|
||
);
|
||
if (data.deep_thought_active) {
|
||
deepToggleBtn.classList.add("active");
|
||
deepToggleIcon.src = "/static/images/ai_chatbot/deep.svg";
|
||
} else {
|
||
deepToggleBtn.classList.remove("active");
|
||
deepToggleIcon.src = "/static/images/ai_chatbot/no-deep.svg";
|
||
}
|
||
deepStatus = data.deep_thought_active;
|
||
} else {
|
||
console.error("Error getting deep thought status:", data.message);
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
console.error("Error:", error);
|
||
});
|
||
}
|
||
|
||
// 设置深度思考状态
|
||
function setDeepThoughtStatus(status) {
|
||
fetch("/set_deep_thought", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({ status: status }), // 传递状态
|
||
})
|
||
.then((response) => response.json())
|
||
.then((data) => {
|
||
if (data.status === "success") {
|
||
console.log(
|
||
"Deep thought status set successfully:",
|
||
data.message
|
||
);
|
||
getDeepThoughtStatus();
|
||
} else {
|
||
console.error("Error setting deep thought status:", data.message);
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
console.error("Error:", error);
|
||
});
|
||
}
|
||
|
||
getDeepThoughtStatus();
|
||
|
||
deepToggleBtn.addEventListener(
|
||
"click",
|
||
throttle(() => {
|
||
if (deepStatus) {
|
||
setDeepThoughtStatus(false);
|
||
} else {
|
||
setDeepThoughtStatus(true);
|
||
}
|
||
}, 500)
|
||
);
|
||
|
||
// 联网搜索功能实现
|
||
const searchToggleBtn = document.querySelector("#search-toggle-btn");
|
||
const searchToggleIcon = document.querySelector("#search-toggle-icon");
|
||
const searchToggleDot = document.querySelector("#search-dot");
|
||
|
||
let searchStatus = false;
|
||
|
||
// 获取联网搜索状态
|
||
function getSearchStatus() {
|
||
fetch("/get_ai_search", {
|
||
method: "GET",
|
||
})
|
||
.then((response) => response.json())
|
||
.then((data) => {
|
||
if (data.status === "success") {
|
||
console.log("Current search status:", data.ai_search_active);
|
||
if (data.ai_search_active) {
|
||
searchToggleBtn.classList.add("active");
|
||
searchToggleIcon.src = "/static/images/ai_chatbot/aisearch.svg";
|
||
} else {
|
||
searchToggleBtn.classList.remove("active");
|
||
searchToggleIcon.src =
|
||
"/static/images/ai_chatbot/no-aisearch.svg";
|
||
}
|
||
searchStatus = data.ai_search_active;
|
||
} else {
|
||
console.error("Error getting search status:", data.message);
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
console.error("Error:", error);
|
||
});
|
||
}
|
||
|
||
// 设置联网搜索状态
|
||
function setSearchStatus(status) {
|
||
fetch("/set_ai_search", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({ status: status }), // 传递状态
|
||
})
|
||
.then((response) => response.json())
|
||
.then((data) => {
|
||
if (data.status === "success") {
|
||
console.log("Search status set successfully:", data.message);
|
||
getSearchStatus();
|
||
} else {
|
||
console.error("Error setting search status:", data.message);
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
console.error("Error:", error);
|
||
});
|
||
}
|
||
|
||
getSearchStatus();
|
||
|
||
searchToggleBtn.addEventListener(
|
||
"click",
|
||
throttle(() => {
|
||
if (searchStatus) {
|
||
setSearchStatus(false);
|
||
} else {
|
||
setSearchStatus(true);
|
||
}
|
||
}, 500)
|
||
);
|
||
</script>
|
||
</body>
|
||
</html>
|