2025-05-27 15:46:31 +08:00

1267 lines
41 KiB
HTML
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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">&#8592;</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>