Files
2025-01-12 08:13:40 +00:00

925 lines
33 KiB
HTML
Raw Permalink 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">
<script src="lang-1.8.js"></script>
<title data-i18n="title">黑盒圆桌</title>
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
max-width: 1000px;
margin: 0 auto;
padding: 50px;
line-height: 1.8;
background-color: #f5f5f5;
color: #333;
}
h1 {
font-size: 3.2em;
color: #1a1a1a;
margin-bottom: 40px;
text-align: center;
letter-spacing: 5px;
font-weight: 700;
}
#question-form {
margin-bottom: 50px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
}
#question-input {
flex-grow: 1;
padding: 15px 20px;
font-size: 16px;
border: none;
border-radius: 25px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-right: 20px;
background-color: #fff;
}
#rounds-input {
width: 70px;
padding: 15px;
font-size: 16px;
border: none;
border-radius: 25px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
text-align: center;
margin-right: 20px;
background-color: #fff;
}
#submit-button {
padding: 15px 30px;
background-color: #1a1a1a;
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease, transform 0.2s ease;
text-transform: uppercase;
letter-spacing: 1px;
}
#submit-button:hover {
background-color: #333;
transform: translateY(-2px);
}
#discussion-output {
margin-top: 40px;
}
.message-container {
display: flex;
align-items: flex-start;
background-color: white;
border-radius: 15px;
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
margin-bottom: 25px;
padding: 25px;
transition: transform 0.2s ease, box-shadow 0.2s ease;
border-left: 4px solid #1a1a1a;
}
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 20px;
object-fit: cover;
}
.message-content {
flex: 1;
}
.message-container:hover {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(0,0,0,0.1);
}
.message-role {
font-weight: 600;
margin-bottom: 15px;
color: #1a1a1a;
font-size: 1.1em;
text-transform: uppercase;
letter-spacing: 1px;
}
.message-text {
white-space: pre-wrap;
margin-bottom: 20px;
line-height: 1.8;
color: #333;
}
.audio-player {
width: 100%;
margin-top: 20px;
border-radius: 25px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.human-message {
background-color: #f9f9f9;
border-left: 4px solid #1a1a1a;
}
.ai-message {
background-color: #fff;
border-left: 4px solid #666;
}
#roles-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 40px;
justify-content: center;
}
.role-tag {
background-color: #fff;
color: #1a1a1a;
padding: 10px 20px;
font-size: 14px;
transition: all 0.2s ease;
text-transform: uppercase;
letter-spacing: 1px;
border-radius: 20px;
cursor: pointer;
border: 1px solid #1a1a1a;
margin-bottom: 10px;
}
.role-tag:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}
.role-tag.selected {
background-color: #1a1a1a;
color: white;
}
@media (max-width: 768px) {
body {
padding: 30px;
}
#question-form {
flex-direction: column;
}
#question-input, #rounds-input, #submit-button {
width: 100%;
margin-right: 0;
margin-bottom: 20px;
}
#roles-list {
justify-content: center;
}
.role-tag {
font-size: 12px;
padding: 8px 16px;
}
}
#session-id {
margin-top: 10px;
font-weight: bold;
}
@media (max-width: 768px) {
#back-to-square {
top: 10px;
left: 10px;
font-size: 14px;
padding: 8px 16px;
}
}
.topic-message {
background-color: #000000;
border: 2px solid #ffffff;
border-radius: 15px;
padding: 25px;
margin: 20px 0;
text-align: center;
box-shadow: 0 4px 15px rgba(255, 255, 255, 0.1);
width: 100%;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
}
.topic-message .message-role {
font-size: 1.2em;
color: #ffffff;
margin-bottom: 0;
text-transform: uppercase;
letter-spacing: 1.5px;
font-weight: bold;
text-align: center;
}
#download-audio-btn {
background-color: #000000;
color: #ffffff;
border: 2px solid #ffffff;
border-radius: 15px;
padding: 25px;
margin: 20px 0;
cursor: pointer;
font-size: 1em;
text-transform: uppercase;
letter-spacing: 1.5px;
transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease;
width: 100%;
box-sizing: border-box;
text-align: center;
display: block;
}
#download-audio-btn:hover {
background-color: #333333;
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(255, 255, 255, 0.2);
}
@media (max-width: 768px) {
.topic-message,
#download-audio-btn {
padding: 20px;
font-size: 0.9em;
}
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
#language-select {
font-size: 14px;
padding: 8px;
}
#back-to-square {
font-size: 14px;
padding: 8px 16px;
}
}
.topic-selector {
display: flex;
justify-content: center;
margin-bottom: 20px;
}
.topic-button {
background-color: white;
color: black;
border: 1px solid #1a1a1a;
border-radius: 25px;
padding: 10px 20px;
margin: 0 10px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease, transform 0.2s ease;
}
.topic-button:hover {
background-color: #f0f0f0;
transform: translateY(-2px);
}
.topic-button.active {
background-color: #1a1a1a;
color: white;
}
/* 添加语言选择器样式 */
.container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
width: 100%;
max-width: 1000px;
box-sizing: border-box;
margin: 0 auto;
}
.right-section {
display: flex;
align-items: center;
}
#back-to-square {
padding: 10px 20px;
background-color: #1a1a1a;
color: white;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease, transform 0.2s ease;
text-transform: uppercase;
letter-spacing: 1px;
width: auto; /* Allow the width to adjust to content */
min-width: 40px; /* Set a minimum width */
}
#back-to-square:hover {
background-color: #333;
transform: translateY(-2px);
}
#language-selector {
margin: 10px;
}
#language-select {
padding: 10px 20px;
font-size: 16px;
border: none;
border-radius: 25px;
background-color: #1a1a1a;
color: white;
cursor: pointer;
transition: background-color 0.3s ease;
width: auto; /* Allow the width to adjust to content */
min-width: 40px; /* Set a minimum width */
}
#language-select:hover {
background-color: #333;
}
#language-select option {
background-color: white;
color: black;
padding: 10px;
}
#language-select:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(255,255,255,0.5);
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
margin-right: 20px;
object-fit: cover;
}
.message-content {
flex: 1;
}
.user-info {
display: flex;
align-items: center;
position: relative;
cursor: pointer;
margin-left: auto; /* 将用户信息推到右侧 */
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #1a1a1a;
color: white;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
font-size: 20px;
margin-right: 10px;
}
.user-dropdown {
display: none;
position: absolute;
top: 100%;
right: 0;
background-color: white;
border: 1px solid #ccc;
border-radius: 5px;
padding: 10px;
z-index: 1000;
}
.user-dropdown.show {
display: block;
}
.logout-btn {
background-color: #1a1a1a;
color: white;
border: none;
padding: 8px 10px; /* Increased padding */
border-radius: 5px;
cursor: pointer;
width: 100%; /* Make the button full width within its container */
min-width: 60px; /* Set a minimum width */
font-size: 13px; /* Increased font size */
transition: background-color 0.3s ease; /* Smooth transition for hover effect */
}
@media (max-width: 768px) {
.container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
width: calc(100% - 30px);
}
#back-to-square {
font-size: 14px;
padding: 8px 16px;
white-space: nowrap;
min-width: 100px;
}
.right-section {
display: flex;
align-items: center;
margin-left: auto; /* 将右侧部分推到最右边 */
}
#language-select {
font-size: 14px;
padding: 8px 16px;
white-space: nowrap;
width: 100px;
min-width: 100px;
margin-right: 10px;
}
.user-info {
display: flex;
align-items: center;
font-size: 14px;
white-space: nowrap;
}
.user-dropdown {
right: 0;
left: auto;
transform: none;
}
.logout-btn {
background-color: #1a1a1a;
color: white;
border: none;
padding: 8px 8px;
border-radius: 5px;
cursor: pointer;
width: auto; /* 调整为自动宽度 */
min-width: 60px;
font-size: 13px;
transition: background-color 0.3s ease;
white-space: nowrap;
}
}
</style>
</head>
<body>
<div class="container">
<button id="back-to-square" data-i18n="backToSquare">返回广场</button>
<div class="right-section">
<div id="language-selector">
<select id="language-select">
<option value="zh">中文</option>
<option value="en">English</option>
<option value="ko">한국어</option>
</select>
</div>
<div class="user-info" id="userInfo">
<div class="user-avatar" id="userAvatar"></div>
<span id="username"></span>
<div class="user-dropdown" id="userDropdown">
<button class="logout-btn" id="logoutBtn" data-i18n="logout">登出</button>
</div>
</div>
</div>
</div>
<h1 data-i18n="title">黑盒圆桌</h1>
<div class="topic-selector">
<button class="topic-button active" data-topic="brainstorm" data-i18n="brainstorm">头脑风暴</button>
<button class="topic-button" data-topic="hallOfFame" data-i18n="hallOfFame">名人堂</button>
</div>
<div id="roles-list"></div>
<form id="question-form">
<input type="text" id="question-input" data-i18n="inputPlaceholder" placeholder="在此处输入问题" required>
<label for="rounds-input" data-i18n="roundsLabel">讨论轮数:</label>
<input type="number" id="rounds-input" value="1" min="1" max="10">
<button type="submit" id="submit-button" data-i18n="startDiscussion">开始讨论</button>
</form>
<div id="session-id"></div>
<div id="discussion-output"></div>
<script>
const BASE_URL = 'https://dev.obscura.work/user';
const questionForm = document.getElementById('question-form');
const questionInput = document.getElementById('question-input');
const roundsInput = document.getElementById('rounds-input');
const discussionOutput = document.getElementById('discussion-output');
const rolesList = document.getElementById('roles-list');
const sessionIdDisplay = document.getElementById('session-id');
const specialRoles = {
brainstorm: ['市场营销专家', '技术专家', '创意专家', '财务专家', '项目规划专家', '数据分析专家'],
hallOfFame: ['马化腾', '李诞', '罗翔', '许知远', '大冰', '余华', '雷军']
};
let currentTopic = 'brainstorm';
let selectedRoles = ['主持人'];
let isAudioReady = false;
// 删除这行: let currentLanguage = localStorage.getItem('preferredLanguage') || 'zh';
// 获取URL参数中的语言设置
const urlParams = new URLSearchParams(window.location.search);
let currentLanguage = urlParams.get('lang') || localStorage.getItem('preferredLanguage') || 'zh';
function initializeRoles() {
rolesList.innerHTML = '';
specialRoles[currentTopic].forEach(role => {
const roleTag = document.createElement('span');
roleTag.className = 'role-tag';
roleTag.textContent = translations[currentLanguage].roles[role] || role;
roleTag.setAttribute('data-role', role);
roleTag.addEventListener('click', () => toggleRole(role, roleTag));
rolesList.appendChild(roleTag);
});
}
function toggleRole(role, element) {
if (selectedRoles.includes(role)) {
selectedRoles = selectedRoles.filter(r => r !== role);
element.classList.remove('selected');
} else {
selectedRoles.push(role);
element.classList.add('selected');
}
}
function updateLanguage(lang) {
currentLanguage = lang;
localStorage.setItem('preferredLanguage', lang);
document.documentElement.lang = lang;
document.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.getAttribute('data-i18n');
if (translations[lang][key]) {
if (element.tagName === 'INPUT' && element.getAttribute('placeholder')) {
element.placeholder = translations[lang][key];
} else {
element.textContent = translations[lang][key];
}
}
});
// 更新角色标签的语言
document.querySelectorAll('.role-tag').forEach(tag => {
const role = tag.getAttribute('data-role');
tag.textContent = translations[lang].roles[role] || role;
});
// 更新URL,不刷新页面
const newUrl = new URL(window.location.href);
newUrl.searchParams.set('lang', lang);
window.history.pushState({}, '', newUrl);
}
document.getElementById('language-select').addEventListener('change', function() {
updateLanguage(this.value);
initializeRoles(); // 重新初始化角色列表
});
questionForm.addEventListener('submit', async (e) => {
e.preventDefault();
const question = questionInput.value;
const maxRounds = roundsInput.value;
const sessionId = generateSessionId();
const selectedLanguage = document.getElementById('language-select').value; // 获取选择的语言
console.log('提交的问题:', question, '最大轮数:', maxRounds, 'Session_ID', sessionId, '选中的角色:', selectedRoles, '选择的语言:', selectedLanguage);
discussionOutput.innerHTML = `<p>${translations[currentLanguage].discussionInProgress}</p>`;
currentSessionId = sessionId;
try {
console.log('发送请求至:', `${BASE_URL}/api/start-discussion`);
const response = await fetch(`${BASE_URL}/api/start-discussion`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
topic: question,
max_rounds: parseInt(maxRounds),
session_id: sessionId,
selected_roles: selectedRoles,
language: selectedLanguage,
current_topic: currentTopic // 添加这一行
}),
});
console.log('收到响应:', response);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let isCompleted = false;
discussionOutput.innerHTML = '';
while (!isCompleted) {
const { done, value } = await reader.read();
if (done) break;
const content = decoder.decode(value);
console.log('Received content:', content);
if (content.includes('[AUDIO_COMPLETED]')) {
console.log('Received AUDIO_COMPLETED message');
const downloadButton = document.createElement('button');
downloadButton.id = 'download-audio-btn';
downloadButton.textContent = translations[currentLanguage].discussionCompleted;
downloadButton.onclick = () => downloadAudio(currentSessionId);
discussionOutput.appendChild(downloadButton);
console.log('Download button added to DOM');
isCompleted = true;
try {
const jsonData = JSON.parse(content);
if (jsonData.audio_task_id) {
handleAudio(jsonData.audio_task_id, completionMessage);
}
} catch (jsonError) {
console.error('Error parsing AUDIO_COMPLETED JSON:', jsonError);
}
break;
}
const lines = content.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
console.log('Parsed data:', data);
try {
const jsonData = JSON.parse(data);
console.log('Parsed JSON data:', jsonData);
if (jsonData.topic) {
const topicContainer = document.createElement('div');
topicContainer.className = 'message-container topic-message';
topicContainer.innerHTML = `
<div class="message-role">${jsonData.topic}</div>
`;
discussionOutput.appendChild(topicContainer);
} else if (jsonData.chunk) {
const messageContainer = document.createElement('div');
messageContainer.className = 'message-container';
const avatarImg = document.createElement('img');
avatarImg.className = 'avatar';
avatarImg.src = `${BASE_URL}/avatar/${jsonData.avatar}`; // 更新头像路径
avatarImg.alt = jsonData.post;
const messageContent = document.createElement('div');
messageContent.className = 'message-content';
messageContent.innerHTML = `
<div class="message-role">${jsonData.post}</div>
<div class="message-text">${jsonData.chunk}</div>
`;
messageContainer.appendChild(avatarImg);
messageContainer.appendChild(messageContent);
discussionOutput.appendChild(messageContainer);
console.log('New message container created with avatar');
if (jsonData.audio_task_id) {
console.log('Audio task ID detected:', jsonData.audio_task_id);
handleAudio(jsonData.audio_task_id, messageContent);
}
}
discussionOutput.scrollTop = discussionOutput.scrollHeight;
console.log('Discussion output scrolled to bottom');
} catch (jsonError) {
console.error('Error parsing JSON:', jsonError, 'Raw data:', data);
}
}
}
console.log('Discussion output content:', discussionOutput.innerHTML);
}
} catch (error) {
console.error('获取操作出错:', error);
discussionOutput.innerHTML += `<p>${translations[currentLanguage].errorOccurred}</p>`;
}
});
async function downloadAudio(sessionId) {
if (!sessionId) {
alert(translations[currentLanguage].startDiscussionFirst);
return;
}
try {
const selectedLanguage = document.getElementById('language-select').value;
const response = await fetch(`${BASE_URL}/api/get-combined-audio/${sessionId}?language=${selectedLanguage}`);
if (response.ok) {
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = `${sessionId}_combined.wav`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
} else if (response.status === 404) {
const data = await response.json();
if (data.detail === "Combined audio not found") {
alert(translations[currentLanguage].audioGenerating);
} else {
alert(translations[currentLanguage].audioNotExist);
}
} else {
throw new Error(translations[currentLanguage].downloadError);
}
} catch (error) {
console.error('下载音频时发生错误:', error);
alert(translations[currentLanguage].downloadError);
}
}
function generateSessionId() {
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);
});
}
async function handleAudio(taskId, messageContainer) {
console.log('Handling audio for task ID:', taskId);
let status = 'queued';
while (status === 'queued' || status === 'processing') {
await new Promise(resolve => setTimeout(resolve, 1000));
try {
const response = await fetch(`${BASE_URL}/tts_result/${taskId}`);
const data = await response.json();
status = data.status;
console.log('Audio status:', status);
} catch (error) {
console.error('Error checking audio status:', error);
return;
}
}
if (status === 'completed') {
try {
console.log('Audio completed, fetching audio file');
const audioResponse = await fetch(`${BASE_URL}/tts_audio/${taskId}`);
const audioBlob = await audioResponse.blob();
const audioUrl = URL.createObjectURL(audioBlob);
const audioPlayer = document.createElement('audio');
audioPlayer.controls = true;
audioPlayer.src = audioUrl;
audioPlayer.className = 'audio-player';
messageContainer.appendChild(audioPlayer);
console.log('Audio player added to message container');
} catch (error) {
console.error('Error fetching or creating audio:', error);
}
} else {
console.error('Audio generation failed');
}
}
document.addEventListener('DOMContentLoaded', () => {
document.getElementById('language-select').value = currentLanguage;
updateLanguage(currentLanguage);
initializeRoles();
});
document.getElementById('back-to-square').addEventListener('click', function(e) {
e.preventDefault();
const lang = document.getElementById('language-select').value;
window.location.href = `https://beta.obscura.work/space.html?lang=${lang}`;
});
document.querySelectorAll('.topic-button').forEach(button => {
button.addEventListener('click', () => {
switchTopic(button.getAttribute('data-topic'));
});
});
function switchTopic(topic) {
currentTopic = topic;
document.querySelectorAll('.topic-button').forEach(button => {
button.classList.remove('active');
});
document.querySelector(`[data-topic="${topic}"]`).classList.add('active');
selectedRoles = ['主持人'];
initializeRoles();
}
// 检查登录状态的函数
async function checkLoginStatus() {
const token = localStorage.getItem('access_token');
if (!token) {
window.location.href = '/login.html';
return;
}
try {
const response = await fetch(`${BASE_URL}/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (!response.ok) {
throw new Error('未登录或会话已过期');
}
// 如果成功,用户已登录
console.log('用户已登录');
} catch (error) {
console.error('检查登录状态时出错:', error);
localStorage.removeItem('access_token');
window.location.href = '/login.html';
}
}
// 添加用户信息和登出功能
const userInfo = document.getElementById('userInfo');
const userDropdown = document.getElementById('userDropdown');
const logoutBtn = document.getElementById('logoutBtn');
const usernameSpan = document.getElementById('username');
const userAvatar = document.getElementById('userAvatar');
async function fetchUserInfo() {
try {
const response = await fetch(`${BASE_URL}/user-info`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
if (response.ok) {
const userData = await response.json();
usernameSpan.textContent = userData.username;
userAvatar.textContent = userData.avatar;
}
} catch (error) {
console.error('Error fetching user info:', error);
}
}
userInfo.addEventListener('click', () => {
userDropdown.classList.toggle('show');
});
logoutBtn.addEventListener('click', async () => {
try {
const response = await fetch(`${BASE_URL}/logout`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
}
});
if (response.ok) {
localStorage.removeItem('access_token');
window.location.href = '/login.html';
}
} catch (error) {
console.error('Error logging out:', error);
}
});
document.addEventListener('DOMContentLoaded', () => {
// 现有的初始化代码
checkLoginStatus();
fetchUserInfo();
});
// 在页面加载时检查登录状态
document.addEventListener('DOMContentLoaded', checkLoginStatus);
</script>
</body>
</html>