593 lines
22 KiB
JavaScript
593 lines
22 KiB
JavaScript
// chat.js
|
|
|
|
// 在文件开头添加这个函数
|
|
function i18n(key) {
|
|
return lang[selectedLang][key] || key;
|
|
}
|
|
|
|
async function showChat() {
|
|
try {
|
|
const userInfo = await fetchUserInfo();
|
|
if (!userInfo) {
|
|
alert(i18n('pleaseLogin'));
|
|
window.location.href = 'login.html'; // 重定向到登录页面
|
|
return;
|
|
}
|
|
|
|
// 检查邮箱验证状态
|
|
const emailVerified = userInfo.email_verified === 'True' || userInfo.email_verified === true;
|
|
console.log('Email Verified:', emailVerified);
|
|
if (!emailVerified) {
|
|
alert(i18n('pleaseVerifyEmail'));
|
|
showUserCenterSection('profile'); // 跳转到个人资料页面
|
|
return;
|
|
}
|
|
|
|
// 检查是否已经设置了 API 密钥
|
|
if (!window.chatAssistant.API_KEY) {
|
|
const apiKey = prompt(i18n('pleaseEnterApiKey'));
|
|
if (!apiKey) {
|
|
alert(i18n('apiKeyRequired'));
|
|
return;
|
|
}
|
|
window.chatAssistant.setApiKey(apiKey);
|
|
}
|
|
|
|
|
|
const chatContent = document.getElementById('chatContent');
|
|
const dashboardContent = document.getElementById('dashboardContent');
|
|
const usersContent = document.getElementById('usersContent');
|
|
const pageTitle = document.getElementById('pageTitle');
|
|
const featureContent = document.getElementById('featureContent');
|
|
const userCenterContent = document.getElementById('userCenterContent');
|
|
const showApiDocs = document.getElementById('showApiDocs');
|
|
const apiDocsContent = document.getElementById('apiDocsContent');
|
|
|
|
if (apiDocsContent) apiDocsContent.style.display = 'none';
|
|
if (featureContent) featureContent.style.display = 'none';
|
|
if (chatContent) chatContent.style.display = 'flex';
|
|
if (dashboardContent) dashboardContent.style.display = 'none';
|
|
if (usersContent) usersContent.style.display = 'none';
|
|
if (userCenterContent) userCenterContent.style.display = 'none';
|
|
if (pageTitle) pageTitle.innerText = i18n('chatAssistant');
|
|
if (showApiDocs) showApiDocs.style.display = 'none';
|
|
|
|
if (window.chatAssistant) {
|
|
// window.chatAssistant.setApiKey(apiKey);
|
|
window.chatAssistant.showWelcomeMessage();
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in showChat:', error);
|
|
alert(i18n('errorGettingUserInfo'));
|
|
}
|
|
}
|
|
|
|
class ChatAssistant {
|
|
constructor() {
|
|
this.API_BASE_URL = 'https://dev.obscura.work/v1_chat';
|
|
this.isRecording = false;
|
|
this.API_KEY = null;
|
|
this.audioChunks = [];
|
|
this.chatHistory = [];
|
|
this.sessionId = this.generateUUID();
|
|
this.onStatusUpdate = null;
|
|
this.onMessageReceived = null;
|
|
this.currentVoice ='default';
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
this.initializeEventListeners();
|
|
});
|
|
}
|
|
setApiKey(apiKey) {
|
|
this.API_KEY = apiKey;
|
|
}
|
|
|
|
generateUUID() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
var r = Math.random() * 16 | 0,
|
|
v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
setVoice(voice) {
|
|
this.currentVoice = voice;
|
|
}
|
|
initializeEventListeners() {
|
|
const micButton = document.getElementById('micButton');
|
|
const sendButton = document.getElementById('sendButton');
|
|
const textInput = document.getElementById('textInput');
|
|
const voiceSelect = document.getElementById('voiceSelect');
|
|
if (voiceSelect) {
|
|
voiceSelect.addEventListener('change', (e) => {
|
|
this.setVoice(e.target.value);
|
|
});
|
|
}
|
|
|
|
if (micButton) micButton.onclick = () => this.toggleMic();
|
|
if (sendButton) sendButton.onclick = () => this.sendTextMessage();
|
|
if (textInput) {
|
|
textInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
this.sendTextMessage();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
addMessage(sender, text) {
|
|
const conversationLog = document.getElementById('conversationLog');
|
|
if (conversationLog) {
|
|
const messageDiv = document.createElement('div');
|
|
messageDiv.classList.add('message', sender === 'user' ? 'user-message' : 'assistant-message');
|
|
messageDiv.textContent = text;
|
|
conversationLog.appendChild(messageDiv);
|
|
conversationLog.scrollTop = conversationLog.scrollHeight;
|
|
return messageDiv;
|
|
} else {
|
|
console.error("conversationLog element not found");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async initializeChat() {
|
|
if (!this.API_KEY) {
|
|
throw new Error(i18n('apiKeyNotSet'));
|
|
}
|
|
try {
|
|
this.updateStatus(i18n('initializingChat'));
|
|
const response = await fetch(`${this.API_BASE_URL}/chat`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
session_id: this.sessionId,
|
|
query: i18n('startConversation')
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
console.error('Server error:', errorData);
|
|
throw new Error(`HTTP error! status: ${response.status}, message: ${JSON.stringify(errorData)}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log("初始化响应:", data);
|
|
this.updateStatus(i18n('chatInitialized'));
|
|
} catch (error) {
|
|
console.error('Error initializing chat:', error);
|
|
this.updateStatus(i18n('errorInitializingChat') + error.message);
|
|
}
|
|
}
|
|
|
|
toggleMic() {
|
|
if (!this.isRecording) {
|
|
this.startRecording();
|
|
document.getElementById('micButton').classList.add('recording');
|
|
} else {
|
|
this.stopRecording();
|
|
document.getElementById('micButton').classList.remove('recording');
|
|
}
|
|
}
|
|
|
|
async startRecording() {
|
|
this.audioChunks = [];
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
this.mediaRecorder = new MediaRecorder(stream);
|
|
|
|
this.mediaRecorder.ondataavailable = event => {
|
|
this.audioChunks.push(event.data);
|
|
};
|
|
|
|
this.mediaRecorder.start();
|
|
this.isRecording = true;
|
|
this.updateStatus(i18n('recordingInProgress'));
|
|
document.getElementById('micButton').classList.add('recording');
|
|
} catch (error) {
|
|
console.error('Error accessing microphone:', error);
|
|
this.updateStatus(i18n('errorAccessingMicrophone'));
|
|
}
|
|
}
|
|
|
|
stopRecording() {
|
|
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
this.mediaRecorder.stop();
|
|
this.isRecording = false;
|
|
this.updateStatus(i18n('recordingStopped'));
|
|
document.getElementById('micButton').classList.remove('recording');
|
|
|
|
this.mediaRecorder.onstop = () => {
|
|
const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
|
|
this.uploadRecording(audioBlob);
|
|
};
|
|
}
|
|
}
|
|
|
|
uploadRecording(audioBlob) {
|
|
const formData = new FormData();
|
|
formData.append('audio', audioBlob, 'recording.wav');
|
|
this.uploadAudio(formData);
|
|
}
|
|
|
|
async uploadAudio(formData) {
|
|
try {
|
|
const response = await fetch(`${this.API_BASE_URL}/asr`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${this.API_KEY}`
|
|
},
|
|
body: formData
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('上传成功:', data);
|
|
this.pollAsrResult(data.task_id);
|
|
} catch (error) {
|
|
console.error('上传失败:', error);
|
|
this.updateStatus(i18n('uploadFailed'));
|
|
}
|
|
}
|
|
|
|
async pollAsrResult(taskId) {
|
|
let attempts = 0;
|
|
const maxAttempts = 30; // 最多轮询60秒(30 * 2秒)
|
|
const pollInterval = setInterval(async () => {
|
|
if (attempts >= maxAttempts) {
|
|
clearInterval(pollInterval);
|
|
this.updateStatus(i18n('asrProcessingTimeout'));
|
|
return;
|
|
}
|
|
attempts++;
|
|
|
|
try {
|
|
console.log(`Polling ASR result for task: ${taskId}, attempt: ${attempts}`);
|
|
const response = await fetch(`${this.API_BASE_URL}/asr_result/${taskId}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${this.API_KEY}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('Received ASR poll response:', data);
|
|
|
|
if (data.status === 'completed') {
|
|
clearInterval(pollInterval);
|
|
console.log('ASR completed, transcription:', data.transcription);
|
|
this.displayTranscription(data.transcription);
|
|
this.processTranscription(data.transcription);
|
|
} else if (data.status === 'failed') {
|
|
clearInterval(pollInterval);
|
|
this.updateStatus(i18n('asrProcessingFailed'));
|
|
} else if (data.status === 'not_found') {
|
|
clearInterval(pollInterval);
|
|
this.updateStatus(i18n('asrTaskNotFound'));
|
|
} else {
|
|
// 其他状态,例如 'queued' 或 'processing'
|
|
this.updateStatus(i18n('asrProcessing') + `(${data.status})`);
|
|
}
|
|
} catch (error) {
|
|
console.error('轮询ASR结果时出错:', error);
|
|
clearInterval(pollInterval);
|
|
this.updateStatus(i18n('getAsrResultError'));
|
|
}
|
|
}, 2000);
|
|
}
|
|
displayTranscription(text) {
|
|
console.log('Displaying transcription:', text);
|
|
this.addMessage('user', text);
|
|
this.chatHistory.push(['user', text]);
|
|
this.updateStatus(i18n('processingTranscription'));
|
|
}
|
|
|
|
async sendTextMessage() {
|
|
const textInput = document.getElementById('textInput');
|
|
const message = textInput.value.trim();
|
|
if (message) {
|
|
console.log("Sending text message:", message);
|
|
this.addMessage('user', message);
|
|
textInput.value = '';
|
|
await this.processTranscription(message, this.currentVoice);
|
|
}
|
|
}
|
|
|
|
|
|
async processTranscription(text, voice) {
|
|
try {
|
|
this.updateStatus(i18n('generatingAIResponse'));
|
|
const chatResponse = await fetch(`${this.API_BASE_URL}/chat`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.API_KEY}`
|
|
},
|
|
body: JSON.stringify({
|
|
session_id: this.sessionId,
|
|
query: text,
|
|
voice: voice || 'default'
|
|
})
|
|
});
|
|
|
|
if (!chatResponse.ok) {
|
|
const errorData = await chatResponse.json();
|
|
console.error('Server error:', errorData);
|
|
throw new Error(`HTTP error! status: ${chatResponse.status}, message: ${JSON.stringify(errorData)}`);
|
|
}
|
|
|
|
const chatData = await chatResponse.json();
|
|
this.pollChatResult(chatData.task_id);
|
|
} catch (error) {
|
|
console.error('Error processing response:', error);
|
|
this.updateStatus(i18n('errorProcessingResponse') + error.message);
|
|
}
|
|
}
|
|
|
|
async pollChatResult(taskId) {
|
|
const pollInterval = setInterval(async () => {
|
|
try {
|
|
const response = await fetch(`${this.API_BASE_URL}/chat_result/${taskId}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${this.API_KEY}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
console.log('收到聊天轮询响应:', data);
|
|
|
|
if (data.status === 'completed') {
|
|
clearInterval(pollInterval);
|
|
if (data.result && data.result.response) {
|
|
const aiResponse = data.result.response;
|
|
const messageElement = this.addMessage('assistant', aiResponse);
|
|
this.chatHistory.push(['assistant', aiResponse]);
|
|
const sentences = this.splitTextIntoSentences(aiResponse);
|
|
this.synthesizeAndPlaySentences([aiResponse], messageElement);
|
|
this.updateStatus(i18n('processingComplete'));
|
|
} else {
|
|
console.error('响应中的结果无效或为空:', data);
|
|
this.updateStatus(i18n('invalidChatResponse'));
|
|
}
|
|
} else if (data.status === 'failed') {
|
|
clearInterval(pollInterval);
|
|
this.updateStatus(i18n('chatProcessingFailed'));
|
|
} else {
|
|
this.updateStatus(i18n('chatProcessing') + `(${data.status})`);
|
|
}
|
|
} catch (error) {
|
|
console.error('轮询聊天结果时出错:', error);
|
|
clearInterval(pollInterval);
|
|
this.updateStatus(i18n('getChatResultError') + error.message);
|
|
}
|
|
}, 1000); // 每1秒轮询一次
|
|
}
|
|
|
|
splitTextIntoSentences(text) {
|
|
return text.split(/(?<=[!?。!?;;])\s*/);
|
|
}
|
|
|
|
async synthesizeAndPlaySentences(sentences, messageElement) {
|
|
const statusElement = document.createElement('div');
|
|
statusElement.textContent = i18n('generatingSpeech');
|
|
statusElement.style.fontStyle = 'italic';
|
|
statusElement.style.color = '#666';
|
|
statusElement.style.marginTop = '10px';
|
|
statusElement.style.fontSize = '0.9em';
|
|
messageElement.appendChild(statusElement);
|
|
|
|
for (let sentence of sentences) {
|
|
try {
|
|
const requestBody = {
|
|
text: sentence,
|
|
voice: this.currentVoice
|
|
};
|
|
console.log('Sending TTS request:', requestBody); // 打印发送的请求体
|
|
|
|
const response = await fetch(`${this.API_BASE_URL}/tts`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${this.API_KEY}`
|
|
},
|
|
body: JSON.stringify(requestBody)
|
|
});
|
|
|
|
console.log('TTS response status:', response.status); // 打印响应状态码
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error('TTS error response:', errorText); // 打印错误响应
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
console.log('TTS response data:', data); // 打印响应数据
|
|
|
|
if (data.status === "completed") {
|
|
// 如果已经存在音频文件,直接获取并播放
|
|
await this.fetchAndPlayAudio(data.task_id, statusElement);
|
|
} else {
|
|
// 否则,开始轮询结果
|
|
await this.pollTtsResult(data.task_id, statusElement);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error in speech synthesis:`, error);
|
|
statusElement.textContent = i18n('errorProcessingResponse') + error.message;
|
|
}
|
|
}
|
|
}
|
|
|
|
async pollTtsResult(taskId, statusElement) {
|
|
const pollInterval = setInterval(async () => {
|
|
try {
|
|
const response = await fetch(`${this.API_BASE_URL}/tts_result/${taskId}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${this.API_KEY}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.status === 'completed') {
|
|
clearInterval(pollInterval);
|
|
await this.fetchAndPlayAudio(taskId, statusElement);
|
|
} else if (data.status === 'failed') {
|
|
clearInterval(pollInterval);
|
|
statusElement.textContent = i18n('ttsProcessingFailed');
|
|
} else {
|
|
statusElement.textContent = i18n('ttsProcessing') + `(${data.status})`;
|
|
}
|
|
} catch (error) {
|
|
console.error(i18n('ttsPollingError'), error);
|
|
clearInterval(pollInterval);
|
|
statusElement.textContent = i18n('ttsPollingError');
|
|
}
|
|
}, 1000); // 每1秒轮询一次
|
|
}
|
|
|
|
async fetchAndPlayAudio(taskId, statusElement) {
|
|
try {
|
|
const audioResponse = await fetch(`${this.API_BASE_URL}/tts_audio/${taskId}`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${this.API_KEY}`
|
|
}
|
|
});
|
|
|
|
if (!audioResponse.ok) {
|
|
throw new Error(`HTTP error! status: ${audioResponse.status}`);
|
|
}
|
|
|
|
const audioBlob = await audioResponse.blob();
|
|
const audioUrl = URL.createObjectURL(audioBlob);
|
|
const audio = new Audio(audioUrl);
|
|
await audio.play();
|
|
statusElement.textContent = i18n('speechGenerationComplete');
|
|
console.log('语音生成完成,正在播放音频'); // 添加这行
|
|
} catch (error) {
|
|
console.error('获取或播放音频时出错:', error);errorFetchingOrPlayingAudio
|
|
statusElement.textContent = i18n('errorFetchingOrPlayingAudio') + error.message;
|
|
}
|
|
}
|
|
|
|
updateStatus(message) {
|
|
if (this.onStatusUpdate) {
|
|
this.onStatusUpdate(message);
|
|
}
|
|
const statusElement = document.getElementById('status');
|
|
if (statusElement) {
|
|
statusElement.textContent = message;
|
|
}
|
|
}
|
|
|
|
showWelcomeMessage() {
|
|
const welcomeMessage = i18n('welcomeMessage');
|
|
this.addMessage('assistant', welcomeMessage);
|
|
this.chatHistory.push(['assistant', welcomeMessage]);
|
|
}
|
|
}
|
|
|
|
// 创建全局实例
|
|
window.chatAssistant = new ChatAssistant();
|
|
|
|
window.initializeChat = async function() {
|
|
console.log("Generated session ID:", window.chatAssistant.sessionId);
|
|
|
|
try {
|
|
window.chatAssistant.onStatusUpdate = (status) => {
|
|
const statusElement = document.getElementById('status');
|
|
if (statusElement) statusElement.textContent = status;
|
|
};
|
|
|
|
window.chatAssistant.onMessageReceived = (sender, message) => {
|
|
console.log(`Received message from ${sender}: ${message}`);
|
|
};
|
|
|
|
await window.chatAssistant.initializeChat();
|
|
|
|
window.chatAssistant.showWelcomeMessage();
|
|
} catch (error) {
|
|
console.error("Error initializing ChatAssistant:", error);
|
|
}
|
|
};
|
|
|
|
window.activateAssistant = function() {
|
|
const dialog = document.getElementById('assistantDialog');
|
|
if (dialog) {
|
|
dialog.style.display = 'block';
|
|
setTimeout(() => {
|
|
dialog.style.opacity = '1';
|
|
dialog.style.transform = 'translate(0, 0)';
|
|
}, 10);
|
|
}
|
|
};
|
|
|
|
window.closeAssistantDialog = function() {
|
|
const dialog = document.getElementById('assistantDialog');
|
|
if (dialog) {
|
|
dialog.style.opacity = '0';
|
|
dialog.style.transform = 'translate(20px, 20px)';
|
|
setTimeout(() => dialog.style.display = 'none', 300);
|
|
}
|
|
};
|
|
|
|
window.sendMessage = async function() {
|
|
if (window.chatAssistant) {
|
|
await window.chatAssistant.sendTextMessage();
|
|
}
|
|
};
|
|
|
|
window.toggleMic = function() {
|
|
if (window.chatAssistant) {
|
|
window.chatAssistant.toggleMic();
|
|
}
|
|
};
|
|
|
|
|
|
// 获取用户信息的函数
|
|
async function fetchUserInfo() {
|
|
try {
|
|
const token = localStorage.getItem('access_token');
|
|
if (!token) {
|
|
console.error('No access token found');
|
|
return null;
|
|
}
|
|
|
|
const response = await fetch(`${BASE_URL}/me`, {
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch user info');
|
|
}
|
|
|
|
const userInfo = await response.json();
|
|
return userInfo;
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching user info:', error);
|
|
// 可以在这里处理错误,比如清除 token 并重定向到登录页面
|
|
localStorage.removeItem('access_token');
|
|
window.location.href = 'login.html'; // 重定向到登录页面
|
|
return null;
|
|
}
|
|
} |