实现聊天
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>语音合成测试</title>
<link rel="stylesheet" href="./asset/iconfont.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, sans-serif;
background-color: #f0f2f5;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
}
.chat-container {
/* width: 360px;
height: 600px; */
width: 100%;
height: 100%;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
/* 聊天头部 */
.chat-header {
padding: 15px;
background-color: #0084ff;
color: white;
border-radius: 10px 10px 0 0;
}
.chat-header h2 {
font-size: 16px;
font-weight: 500;
}
/* 聊天消息区域 */
.chat-messages {
flex: 1;
padding: 15px;
overflow-y: auto;
}
.message {
margin-bottom: 15px;
display: flex;
flex-direction: column;
}
.message-content {
padding: 10px;
border-radius: 15px;
margin: 2px 0;
animation: fadeIn 0.3s ease;
white-space: pre-line;
}
.message-content+.iconfont {
margin-left: 10px;
cursor: pointer;
}
.assistant {
align-items: flex-start;
}
.user {
/* align-items: flex-end; */
align-items: flex-start;
}
.assistant .message-content {
background-color: #f0f0f0;
color: #333;
border-bottom-left-radius: 5px;
}
.user .message-content {
background-color: #0084ff;
color: white;
border-bottom-right-radius: 5px;
}
.message-time {
font-size: 12px;
color: #999;
margin: 2px 5px;
}
/* 输入区域 */
.chat-input {
padding: 15px;
border-top: 1px solid #eee;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 0 0 10px 10px;
}
.chat-input input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 20px;
margin-right: 10px;
outline: none;
transition: border-color 0.3s;
}
.chat-input input:focus {
border-color: #0084ff;
}
.chat-input button {
padding: 10px 20px;
background-color: #0084ff;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
transition: background-color 0.3s;
}
.chat-input button:hover {
background-color: #0073e6;
}
.chat-input button.stop {
background-color: #b8b8b8;
display: none;
}
/* 动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 滚动条样式 */
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-messages::-webkit-scrollbar-track {
background: #f1f1f1;
}
.chat-messages::-webkit-scrollbar-thumb {
background: #888;
border-radius: 3px;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
background: #555;
}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<h2>聊天室</h2>
</div>
<div class="chat-messages">
<div class="message user">
<div style="display: flex; align-items: center;">
<div class="message-content">你好!</div>
<span class="iconfont icon-a-zhuanyuyin3x"></span>
</div>
<span class="message-time">10:00</span>
</div>
<div class="message assistant">
<div class="w-80" style="display: flex; align-items: center;">
<div class="message-content">你好!很高兴见到你。</div>
<span class="iconfont icon-a-zhuanyuyin3x"></span>
</div>
<span class="message-time">10:01</span>
</div>
<!-- <div class="message user">
<div style="display: flex; align-items: center;">
<div class="message-content">今天天气真不错!</div>
<span class="iconfont icon-a-zhuanyuyin3x"></span>
</div>
<span class="message-time">10:02</span>
</div>
<div class="message assistant">
<div style="display: flex; align-items: center;">
<div class="message-content">是的,非常适合出去走走。</div>
<span class="iconfont icon-a-zhuanyuyin3x"></span>
</div>
<span class="message-time">10:03</span>
</div> -->
</div>
<div class="chat-input">
<input type="text" placeholder="输入消息...">
<button class="send">发送</button>
<button class="stop">停止</button>
</div>
</div>
<script>
// 添加发送消息的功能
const input = document.querySelector('.chat-input input');
const send = document.querySelector('.chat-input .send');
const messages = document.querySelector('.chat-messages');
const stop = document.querySelector('.chat-input .stop');
const chat_messages = document.querySelector('.chat-messages');
let controller = null;
async function text_to_voice(text) {
// 发送消息
url = 'http://localhost:8086/text_to_speech';
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: text })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
console.log(data);
const binary = atob(data.data);
const uint8Array = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
uint8Array[i] = binary.charCodeAt(i);
}
console.log("uint8Array:", uint8Array);
const blob = new Blob([uint8Array], { type: 'audio/mp3' });
console.log("blob:", blob);
const audioUrl = URL.createObjectURL(blob);
// 将blob存到本地
// const encryptString = btoa(window.encodeURI(text));
// console.log(encryptString);
// const a = document.createElement('a');
// a.href = audioUrl;
// a.download = encryptString + '.mp3';
// a.click();
// window.URL.revokeObjectURL(audioUrl);
const audio = new Audio();
audio.src = audioUrl;
await audio.play();
}
async function postMessage(text) {
send.style.display = "none";
stop.style.display = "block";
const messages = []
// 循环打印chat_messages下的所有元素
for (let i = 0; i < chat_messages.children.length; i++) {
// 判断元素是否包含某个类
messages.push({
role: chat_messages.children[i].classList.contains('assistant') ? "assistant" : "user",
content: chat_messages.children[i].querySelector('.message-content').innerText
});
}
// 发送消息
url = 'http://localhost:8086/chat';
try {
controller = new AbortController();
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ messages: messages }),
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader()
const decoder = new TextDecoder();
while (true) {
const { value, done } = await reader.read()
if (done) break;
const chunk = decoder.decode(value);
console.log(chunk);
sendMessage(chunk, "assistant")
}
} catch (e) {
console.log("终止请求!", e);
}
send.style.display = "block";
stop.style.display = "none";
controller = null;
}
function sendMessage(text, type = "user") {
// const text = input.value.trim();
if (text) {
const now = new Date();
const time = `${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}`;
const messageHTML = `
<div class="message ${type}">
<div class="w-80" style="display: flex; align-items: center;">
<div class="message-content">${text}</div>
<span class="iconfont icon-a-zhuanyuyin3x"></span>
</div>
<span class="message-time">${time}</span>
</div>
`;
messages.insertAdjacentHTML('beforeend', messageHTML);
input.value = '';
// 滚动到底部
messages.scrollTop = messages.scrollHeight;
}
}
// 点击发送按钮发送消息
send.addEventListener('click', e => {
console.log(send.style.display);
if (send.style.display !== "none") {
sendMessage(input.value.trim());
postMessage(input.value.trim());
}
});
// 按回车键发送消息
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter' && send.style.display !== "none") {
sendMessage(input.value.trim());
postMessage(input.value.trim());
}
});
stop.addEventListener('click', e => {
// 停止网络请求
controller.abort();
});
messages.addEventListener('click', async e => {
if (e.target.classList.contains('iconfont')) {
// 打印上一个兄弟节点
// console.log(e.target.previousElementSibling);
// 添加属性 audio_src
// e.target.setAttribute('audio_src', audio_src);
text_to_voice(e.target.previousElementSibling.innerText,);
}
})
</script>
</body>
</html>