Initial commit
@@ -0,0 +1,94 @@
|
||||
# 黑盒智能开发者平台
|
||||
|
||||
## 项目简介
|
||||
这是一个基于Web的开发者平台,提供AI模型调用、用户管理、API密钥管理等功能。平台支持多语言(中文、英文、韩文),并提供完整的用户认证和授权系统。
|
||||
|
||||
## 主要功能
|
||||
- 用户认证与授权
|
||||
- 用户注册/登录
|
||||
- 邮箱验证
|
||||
- 手机号验证
|
||||
- 个人资料管理
|
||||
|
||||
- API管理
|
||||
- API密钥生成与管理
|
||||
- API使用统计
|
||||
- API文档浏览
|
||||
|
||||
- AI模型调用
|
||||
- 姿态分析
|
||||
- 跌倒检测
|
||||
- 人脸检测
|
||||
- 目标检测
|
||||
- MiniCPM内容分析
|
||||
- Qwen内容分析
|
||||
- OCR分析
|
||||
|
||||
- 实时聊天
|
||||
- 文字聊天
|
||||
- 语音输入
|
||||
- 语音合成
|
||||
|
||||
- 管理功能
|
||||
- 用户管理
|
||||
- 使用统计
|
||||
- 仪表盘数据
|
||||
|
||||
|
||||
## 项目结构
|
||||
user/
|
||||
├── center.js # 用户中心相关功能
|
||||
├── chat.js # 聊天功能实现
|
||||
├── docs.js # API文档相关功能
|
||||
├── lang.js # 多语言支持
|
||||
├── manage.js # 用户管理功能
|
||||
├── module.js # AI模型调用模块
|
||||
├── platform.html # 主平台页面
|
||||
├── login.html # 登录页面
|
||||
└── user/ # 后端服务
|
||||
├── main.py # 主应用程序
|
||||
├── .env # 环境配置
|
||||
└── requirements.txt # 依赖包列表
|
||||
└── picture/ # 用户头像
|
||||
|
||||
|
||||
## 安装与部署
|
||||
# 1. 安装后端依赖:
|
||||
pip install -r requirements.txt
|
||||
# 2. 配置环境变量:
|
||||
- 在`.env`文件中填写必要的配置信息(数据库、Redis、SMTP等)
|
||||
# 3. 启动后端服务:
|
||||
uvicorn main:app --host 0.0.0.0 --port 8000
|
||||
# 4. 部署web文件到Web服务器
|
||||
|
||||
## API基础URL
|
||||
https://dev.obscura.work/v1
|
||||
https://dev.obscura.work/v1_chat
|
||||
|
||||
## 使用说明
|
||||
### main.py # 主应用程序
|
||||
1. 服务器:150.158.144.159
|
||||
2. PORT = 8000
|
||||
3. 数据库:
|
||||
1. SQL: sh-cdb-4ln6r8y0.sql.tencentcdb.com:28234/minio
|
||||
2. Redis:
|
||||
REDIS_HOST=10.0.4.17
|
||||
REDIS_PORT=13003
|
||||
REDIS_DB: 0:缓存;1:用户注册;2:API密钥;3:API使用统计;
|
||||
4. 登录认证:SUPABASE
|
||||
|
||||
## 安全说明
|
||||
- 使用JWT进行用户认证
|
||||
- 支持API密钥认证
|
||||
- 邮箱验证
|
||||
- 密码强度要求:
|
||||
- 至少8个字符
|
||||
- 包含大小写字母
|
||||
- 包含数字
|
||||
- 包含特殊字符
|
||||
|
||||
## 注意事项
|
||||
- 确保Redis服务正常运行
|
||||
- 配置正确的SMTP服务以启用邮件功能
|
||||
- 确保数据库连接信息正确
|
||||
- 注意保护`.env`文件中的敏感信息
|
||||
@@ -0,0 +1,812 @@
|
||||
function i18n(key) {
|
||||
return lang[selectedLang][key] || key;
|
||||
}
|
||||
|
||||
// 显示用户中心特定部分
|
||||
async function showUserCenterSection(section) {
|
||||
try {
|
||||
const userInfo = await fetchUserInfo();
|
||||
if (!userInfo) {
|
||||
alert(lang[selectedLang].pleaseLogin);
|
||||
window.location.href = 'login.html'; // 重定向到登录页面
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查邮箱验证状态
|
||||
const emailVerified = userInfo.email_verified === 'True' || userInfo.email_verified === true;
|
||||
console.log('Email Verified:', emailVerified); // 打印邮箱验证状态
|
||||
if (!emailVerified && section !== 'profile') {
|
||||
alert(lang[selectedLang].pleaseVerifyEmail);
|
||||
section = 'profile'; // 如果邮箱未验证,强制显示个人资料页面
|
||||
// showUserCenterSection('profile'); // 跳转到个人资料页面
|
||||
return;
|
||||
}
|
||||
|
||||
const dashboardContent = document.getElementById('dashboardContent');
|
||||
const usersContent = document.getElementById('usersContent');
|
||||
const chatContent = document.getElementById('chatContent');
|
||||
const featureContent = document.getElementById('featureContent');
|
||||
const userCenterContent = document.getElementById('userCenterContent');
|
||||
const pageTitle = document.getElementById('pageTitle');
|
||||
const showApiDocs = document.getElementById('showApiDocs');
|
||||
const apiDocsContent = document.getElementById('apiDocsContent');
|
||||
|
||||
|
||||
if (apiDocsContent) apiDocsContent.style.display = 'none';
|
||||
if (dashboardContent) dashboardContent.style.display = 'none';
|
||||
if (usersContent) usersContent.style.display = 'none';
|
||||
if (chatContent) chatContent.style.display = 'none';
|
||||
if (featureContent) featureContent.style.display = 'none';
|
||||
if (userCenterContent) userCenterContent.style.display = 'block';
|
||||
if (showApiDocs) showApiDocs.style.display = 'none';
|
||||
|
||||
// 根据选择的部分设置相应的标题
|
||||
if (pageTitle) {
|
||||
switch(section) {
|
||||
case 'profile':
|
||||
pageTitle.innerText = i18n('personalInfo');
|
||||
break;
|
||||
case 'apiKey':
|
||||
pageTitle.innerText = i18n('apiKeys');
|
||||
break;
|
||||
case 'apiUsage':
|
||||
pageTitle.innerText = i18n('modelCalls');
|
||||
break;
|
||||
default:
|
||||
pageTitle.innerText = i18n('userCenter');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const navButtons = document.querySelectorAll('.user-center-nav-btn');
|
||||
const sections = document.querySelectorAll('.user-center-section');
|
||||
|
||||
navButtons.forEach(btn => btn.classList.remove('active'));
|
||||
sections.forEach(sec => sec.classList.remove('active'));
|
||||
|
||||
const targetButton = document.querySelector(`.user-center-nav-btn[data-target="${section}"]`);
|
||||
const targetSection = document.getElementById(`${section}Section`);
|
||||
|
||||
if (targetButton) targetButton.classList.add('active');
|
||||
if (targetSection) targetSection.classList.add('active');
|
||||
|
||||
if (section === 'profile') {
|
||||
loadUserProfile();
|
||||
} else if (section === 'apiKey') {
|
||||
loadApiKeys();
|
||||
} else if (section === 'apiUsage') {
|
||||
loadapiUsage();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in showFeature:', error);
|
||||
alert(lang[selectedLang].errorGettingUserInfo);
|
||||
}
|
||||
}
|
||||
// 将 showUserCenterSection 函数添加到全局作用域
|
||||
window.showUserCenterSection = showUserCenterSection;
|
||||
|
||||
const BASE_URL = 'https://user.obscura.work/user';
|
||||
|
||||
|
||||
function updateAllAvatars(avatarUrl) {
|
||||
const avatars = document.querySelectorAll('.user-avatar, #currentAvatar');
|
||||
avatars.forEach(avatar => {
|
||||
avatar.src = avatarUrl || 'picture/cn__03041_.png';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 加载用户个人资料
|
||||
async function loadUserProfile() {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const userData = await response.json();
|
||||
const profileSection = document.getElementById('profileSection');
|
||||
updateAllAvatars(userData.avatar)
|
||||
profileSection.innerHTML = `
|
||||
<div class="profile-container">
|
||||
<div class="profile-info">
|
||||
<h3>${i18n("basicInfo")}</h3>
|
||||
<form id="profileForm">
|
||||
<div class="form-group">
|
||||
<label for="username">${i18n("username")}</label>
|
||||
<input type="text" id="username" value="${userData.username}" readonly>
|
||||
<p class="field-description">${i18n("usernameDescription")}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">${i18n("email")}</label>
|
||||
<input type="email" id="email" value="${userData.email}">
|
||||
<p class="field-description">${i18n("emailDescription")}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="phone_number">${i18n("phoneNumber")}</label>
|
||||
<input type="tel" id="phone_number" value="${userData.phone_number}">
|
||||
</div>
|
||||
<button type="submit" class="btn-update">${i18n("updateProfile")}</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="profile-picture">
|
||||
<img src="${userData.avatar || 'picture/cn__03041_.png'}" alt="用户头像" id="currentAvatar">
|
||||
<button class="btn-update" onclick="openAvatarModal()">${i18n("editAvatar")}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="verification-card">
|
||||
<h3>${i18n("userVerification")}</h3>
|
||||
<div class="verification-content">
|
||||
<p>${i18n("emailVerification")}<span id="emailStatus">${userData.email_verified ? i18n("verified") : i18n("unverified")}</span></p>
|
||||
${userData.email_verified ? '' : `
|
||||
<button type="button" id="sendVerificationEmail" class="btn-verify">${i18n("sendVerificationEmail")}</button>
|
||||
<div id="verificationCodeSection" style="display: none;">
|
||||
<input type="text" id="verificationCode" name="email_verification_code" autocomplete="off" placeholder="${i18n("enterVerificationCode")}">
|
||||
<button type="button" id="submitVerificationCode" class="btn-verify">${i18n("submitVerificationCode")}</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
<div class="verification-content" style="margin-top: 15px;">
|
||||
<p>${i18n("phoneVerification")}<span id="emailStatus">${userData.phone_verified ? i18n("verified") : i18n("unverified")}</span></p>
|
||||
${userData.phone_verified ? '' : `
|
||||
<button type="button" id="sendPhoneVerification" class="btn-verify">${i18n("sendPhoneVerification")}</button>
|
||||
<div id="phoneVerificationCodeSection" style="display: none;">
|
||||
<input type="text" id="phoneVerificationCode" placeholder="${i18n("enterPhoneVerificationCode")}">
|
||||
<button type="button" id="submitPhoneVerificationCode" class="btn-verify">${i18n("submitPhoneVerificationCode")}</button>
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('profileForm').addEventListener('submit', updateProfile);
|
||||
|
||||
if (!userData.email_verified) {
|
||||
document.getElementById('sendVerificationEmail').addEventListener('click', sendVerificationEmail);
|
||||
document.getElementById('submitVerificationCode').addEventListener('click', submitVerificationCode);
|
||||
}
|
||||
if (!userData.phone_verified) {
|
||||
document.getElementById('sendPhoneVerification').addEventListener('click', sendPhoneVerification);
|
||||
document.getElementById('submitPhoneVerificationCode').addEventListener('click', submitPhoneVerificationCode);
|
||||
}
|
||||
} else {
|
||||
console.error(i18n('loadProfileFailed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('loadProfileError'), error);
|
||||
}
|
||||
}
|
||||
|
||||
// 提交验证码
|
||||
async function sendVerificationEmail() {
|
||||
const sendButton = document.getElementById('sendVerificationEmail');
|
||||
if (sendButton.disabled) {
|
||||
return; // 如果按钮已经被禁用,直接返回
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/send-verification-email`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
alert(i18n('verificationEmailSent'));
|
||||
document.getElementById('verificationCodeSection').style.display = 'block';
|
||||
|
||||
// 禁用按钮并开始倒计时
|
||||
sendButton.disabled = true;
|
||||
let countdown = 60;
|
||||
sendButton.textContent = `${i18n('resend')} (${countdown}s)`;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
countdown--;
|
||||
if (countdown <= 0) {
|
||||
clearInterval(timer);
|
||||
sendButton.disabled = false;
|
||||
sendButton.textContent = i18n('sendVerificationEmail');
|
||||
} else {
|
||||
sendButton.textContent = `${i18n('resend')} (${countdown}s)`;
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`${i18n('sendVerificationEmailFailed')}${errorData.detail || i18n('unknownError')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('sendVerificationEmailError'), error);
|
||||
alert(i18n('sendVerificationEmailError'));
|
||||
}
|
||||
}
|
||||
|
||||
async function submitVerificationCode() {
|
||||
const verificationCode = document.getElementById('verificationCode').value;
|
||||
console.log(i18n('submittingVerificationCode'), verificationCode);
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/verify-email`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: `verification_code=${encodeURIComponent(verificationCode)}`
|
||||
});
|
||||
if (response.ok) {
|
||||
alert(i18n('emailVerificationSuccess'));
|
||||
loadUserProfile();
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
console.error('Error response:', errorData);
|
||||
alert(`${i18n('verificationFailed')}${errorData.detail || i18n('unknownError')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('submitVerificationCodeError'), error);
|
||||
alert(i18n('submitVerificationCodeError'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 新增发送手机验证短信的函数
|
||||
async function sendPhoneVerification() {
|
||||
const sendButton = document.getElementById('sendPhoneVerification');
|
||||
if (sendButton.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/send-phone-verification`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
alert(i18n('verificationSMSSent'));
|
||||
document.getElementById('phoneVerificationCodeSection').style.display = 'block';
|
||||
|
||||
sendButton.disabled = true;
|
||||
let countdown = 60;
|
||||
sendButton.textContent = `${i18n('resend')} (${countdown}s)`;
|
||||
|
||||
const timer = setInterval(() => {
|
||||
countdown--;
|
||||
if (countdown <= 0) {
|
||||
clearInterval(timer);
|
||||
sendButton.disabled = false;
|
||||
sendButton.textContent = i18n('sendPhoneVerification');
|
||||
} else {
|
||||
sendButton.textContent = `${i18n('resend')} (${countdown}s)`;
|
||||
}
|
||||
}, 1000);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`${i18n('sendVerificationSMSFailed')}${errorData.detail || i18n('unknownError')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('sendVerificationSMSError'), error);
|
||||
alert(i18n('sendVerificationSMSError'));
|
||||
}
|
||||
}
|
||||
|
||||
// 新增提交手机验证码的函数
|
||||
async function submitPhoneVerificationCode() {
|
||||
const verificationCode = document.getElementById('phoneVerificationCode').value;
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/verify-phone`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: `verification_code=${encodeURIComponent(verificationCode)}`
|
||||
});
|
||||
if (response.ok) {
|
||||
alert(i18n('phoneVerificationSuccess'));
|
||||
loadUserProfile(); // 重新加载用户资料以更新验证状态
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`${i18n('verificationFailed')}${errorData.detail || i18n('unknownError')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('submitPhoneVerificationCodeError'), error);
|
||||
alert(i18n('submitPhoneVerificationCodeError'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// / 打开头像选择模态框
|
||||
function openAvatarModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'avatar-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="avatar-modal-content">
|
||||
<h3>${i18n("selectAvatar")}</h3>
|
||||
<div id="avatarOptions"></div>
|
||||
<button onclick="closeAvatarModal()">${i18n("close")}</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
loadAvatarOptions();
|
||||
}
|
||||
|
||||
// 加载头像选项
|
||||
async function loadAvatarOptions() {
|
||||
try {
|
||||
const avatars = [
|
||||
'picture/cn__02504_.png',
|
||||
'picture/cn__02505_.png',
|
||||
'picture/cn__02506_.png',
|
||||
'picture/cn__02507_.png',
|
||||
'picture/cn__02510_.png',
|
||||
'picture/cn__02511_.png',
|
||||
'picture/cn__02512_.png',
|
||||
'picture/cn__03018_.png',
|
||||
'picture/cn__03040_.png',
|
||||
'picture/cn__03041_.png',
|
||||
'picture/cn__03042_.png',
|
||||
'picture/cn__03044_.png',
|
||||
'picture/cn__03059_.png'
|
||||
];
|
||||
const avatarOptions = document.getElementById('avatarOptions');
|
||||
avatarOptions.innerHTML = avatars.map(avatar => `
|
||||
<img src="${avatar}" alt="头像选项" onclick="selectAvatar('${avatar}')" class="avatar-option">
|
||||
`).join('');
|
||||
} catch (error) {
|
||||
console.error(i18n('loadAvatarOptionsError'), error);
|
||||
}
|
||||
}
|
||||
|
||||
// 选择头像
|
||||
function selectAvatar(avatarUrl) {
|
||||
document.getElementById('currentAvatar').src = avatarUrl;
|
||||
document.getElementById('userAvatar').src = avatarUrl;
|
||||
closeAvatarModal();
|
||||
updateUserAvatar(avatarUrl); // 添加这行来保存头像更改
|
||||
}
|
||||
|
||||
async function updateUserAvatar(avatarUrl) {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/me`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ avatar: avatarUrl })
|
||||
});
|
||||
if (response.ok) {
|
||||
console.log(i18n('avatarUpdateSuccess'));
|
||||
updateAllAvatars(avatarUrl);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
console.error(i18n('avatarUpdateFailed'), errorData);
|
||||
alert(`${i18n('avatarUpdateFailed')}${errorData.detail || i18n('unknownError')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('avatarUpdateError'), error);
|
||||
alert(i18n('avatarUpdateError'));
|
||||
}
|
||||
}
|
||||
// 关闭头像选择模态框
|
||||
function closeAvatarModal() {
|
||||
const modal = document.querySelector('.avatar-modal');
|
||||
if (modal) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async function updateProfile(event) {
|
||||
event.preventDefault();
|
||||
const email = document.getElementById('email').value;
|
||||
const phone_number = document.getElementById('phone_number').value;
|
||||
const avatar = document.getElementById('currentAvatar').src;
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/me`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
},
|
||||
body: JSON.stringify({ email, phone_number, avatar })
|
||||
});
|
||||
if (response.ok) {
|
||||
alert(i18n('profileUpdateSuccess'));
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`${i18n('profileUpdateFailed')}${errorData.detail || i18n('unknownError')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('profileUpdateError'), error);
|
||||
alert(i18n('profileUpdateError'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 遮蔽 API 密钥
|
||||
function maskApiKey(key) {
|
||||
if (!key) return i18n('invalidApiKey');
|
||||
return key.substring(0, 4) + '*'.repeat(Math.max(0, key.length - 8)) + key.substring(Math.max(0, key.length - 4));
|
||||
}
|
||||
|
||||
// 加载 API 密钥
|
||||
async function loadApiKeys() {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api-keys`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const apiKeys = await response.json();
|
||||
const apiKeyContent = document.getElementById('apiKeyContent');
|
||||
apiKeyContent.innerHTML = `
|
||||
<table class="api-key-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>${i18n("apiKeyTable")}</th>
|
||||
<th>${i18n("createdAt")}</th>
|
||||
<th>${i18n("expiresAt")}</th>
|
||||
<th>${i18n("operation")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="apiKeyTableBody">
|
||||
${apiKeys.map((key, index) => {
|
||||
const actualKey = key.key.replace('api_key:', '');
|
||||
return `
|
||||
<tr>
|
||||
<td>
|
||||
<i class="fas fa-eye toggle-visibility" onclick="toggleApiKeyVisibility(${index}, '${actualKey}')"></i>
|
||||
</td>
|
||||
<td>
|
||||
<span id="apiKey${index}" class="api-key-value" onclick="copyToClipboard('${actualKey}')">${maskApiKey(actualKey)}</span>
|
||||
</td>
|
||||
<td>${new Date(key.created_at).toLocaleString()}</td>
|
||||
<td>${new Date(key.expires_at).toLocaleString()}</td>
|
||||
<td>
|
||||
<button class="btn-delete" onclick="deleteApiKey('${actualKey}')">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
<button class="btn-generate-key" onclick="generateNewApiKey()">${i18n("createNewApiKey")}</button>
|
||||
<button class="btn-refresh" onclick="refreshApiKeys()">${i18n("refreshList")}</button>
|
||||
`;
|
||||
} else {
|
||||
console.error(i18n('loadApiKeysError'), response.status, response.statusText);
|
||||
const apiKeyContent = document.getElementById('apiKeyContent');
|
||||
apiKeyContent.innerHTML = `<p>${i18n('loadApiKeysFailed')}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('loadApiKeysError'), error);
|
||||
const apiKeyContent = document.getElementById('apiKeyContent');
|
||||
apiKeyContent.innerHTML = `<p>${i18n('loadApiKeysFailed')}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 切换 API 密钥可见性
|
||||
function toggleApiKeyVisibility(index, fullKey) {
|
||||
const keyElement = document.getElementById(`apiKey${index}`);
|
||||
if (keyElement.textContent === fullKey) {
|
||||
keyElement.textContent = maskApiKey(fullKey);
|
||||
} else {
|
||||
keyElement.textContent = fullKey;
|
||||
}
|
||||
}
|
||||
|
||||
// 复制到剪贴板
|
||||
function copyToClipboard(text) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
alert(i18n('apiKeyCopied'));
|
||||
}).catch(err => {
|
||||
console.error(i18n('copyTextFailed'), err);
|
||||
});
|
||||
}
|
||||
|
||||
// 生成新的 API 密钥
|
||||
async function generateNewApiKey() {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api-keys`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const newKey = await response.json();
|
||||
alert(`${i18n('newApiKeyGenerated')}${newKey.api_key}\n${i18n('expirationTime')}${new Date(newKey.expires_at).toLocaleString()}`);
|
||||
loadApiKeys(); // 重新加载 API 密钥列表
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`${i18n('generateNewApiKeyFailed')}${errorData.detail || i18n('unknownError')}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('generateNewApiKeyError'), error);
|
||||
alert(i18n('generateNewApiKeyFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
// 修改 deleteApiKey 函数
|
||||
async function deleteApiKey(keyId) {
|
||||
const confirmed = confirm(i18n('confirmDeleteApiKey'));
|
||||
if (confirmed) {
|
||||
try {
|
||||
const encodedKeyId = encodeURIComponent(keyId);
|
||||
const response = await fetch(`${BASE_URL}/api-keys/${encodedKeyId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
alert(data.message);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
alert(`${i18n('deleteApiKeyFailed')}${errorData.detail || i18n('unknownError')}`);
|
||||
}
|
||||
if (data.action === 'clear_api_info') {
|
||||
await loadApiKeys(); // 重新加载API密钥列表
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('deleteApiKeyError'), error);
|
||||
alert(i18n('deleteApiKeyFailed'));
|
||||
} finally {
|
||||
await loadApiKeys(); // 无论成功与否,都重新加载API密钥列表
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 新增清除缓存并重新加载的函数
|
||||
// 确保 refreshApiKeys 函数被正确定义
|
||||
async function refreshApiKeys() {
|
||||
try {
|
||||
await fetch(`${BASE_URL}/clear-cache`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
console.log(i18n('cacheCleared'));
|
||||
} catch (error) {
|
||||
console.error(i18n('clearCacheFailed'), error);
|
||||
} finally {
|
||||
await loadApiKeys();
|
||||
console.log(i18n('apiKeyListRefreshed'));
|
||||
}
|
||||
}
|
||||
|
||||
// 加载模型调用信息
|
||||
async function loadapiUsage() {
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/api-usage`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const usageData = await response.json();
|
||||
const apiUsageContent = document.getElementById('apiUsageContent');
|
||||
|
||||
apiUsageContent.innerHTML = `
|
||||
<div class="usage-tabs">
|
||||
<h3 id="apiUsageBtn" class="tab-btn active" onclick="showApiUsage()">${i18n("apiUsage")}</h3>
|
||||
<h3 id="modelUsageBtn" class="tab-btn" onclick="showModelUsage()">${i18n("modelUsage")}</h3>
|
||||
</div>
|
||||
<div id="apiUsageTable" class="tab-content active">
|
||||
<table class="rate-limit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>API KEY</th>
|
||||
<th>${i18n("tokensUsed")}</th>
|
||||
<th>${i18n("totalTokens")}</th>
|
||||
<th>${i18n("lastUsedAt")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="apiUsageTableBody">
|
||||
${Object.keys(usageData).length === 0 ?
|
||||
`<tr><td colspan="5">${i18n("noUsageRecord")}</td></tr>` :
|
||||
Object.entries(usageData).map(([key, usage], index) => `
|
||||
<tr>
|
||||
<td>
|
||||
<i class="fas fa-eye toggle-visibility" onclick="toggleApiKeyVisibility(${index}, '${key}')"></i>
|
||||
</td>
|
||||
<td>
|
||||
<span id="apiKey${index}" class="api-key-value">${maskApiKey(key)}</span>
|
||||
</td>
|
||||
<td>${usage.tokens_used || 0} token</td>
|
||||
<td>${usage.total_tokens || 0} token</td>
|
||||
<td>${usage.last_used_at ? new Date(usage.last_used_at).toLocaleString() : i18n("unused")}</td>
|
||||
</tr>
|
||||
`).join('')
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="modelUsageTable" class="tab-content">
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 初始显示API使用情况
|
||||
showApiUsage();
|
||||
} else {
|
||||
console.error(i18n('loadModelCallInfoError'), response.status, response.statusText);
|
||||
const apiUsageContent = document.getElementById('apiUsageContent');
|
||||
apiUsageContent.innerHTML = `<p>${i18n('loadModelCallInfoFailed')}</p>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('loadModelCallInfoError'), error);
|
||||
const apiUsageContent = document.getElementById('apiUsageContent');
|
||||
apiUsageContent.innerHTML = `
|
||||
<p>${i18n('loadModelCallInfoFailed')}</p>
|
||||
<button onclick="loadapiUsage()">${i18n('reload')}</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 显示 API 使用情况
|
||||
function showApiUsage() {
|
||||
document.getElementById('apiUsageBtn').classList.add('active');
|
||||
document.getElementById('modelUsageBtn').classList.remove('active');
|
||||
document.getElementById('apiUsageTable').classList.add('active');
|
||||
document.getElementById('modelUsageTable').classList.remove('active');
|
||||
|
||||
// 添加动画效果
|
||||
document.getElementById('apiUsageTable').style.animation = 'none';
|
||||
setTimeout(() => {
|
||||
document.getElementById('apiUsageTable').style.animation = 'fadeIn 0.5s ease-in-out';
|
||||
}, 10);
|
||||
|
||||
}
|
||||
|
||||
// 显示模型使用情况
|
||||
async function showModelUsage() {
|
||||
document.getElementById('apiUsageBtn').classList.remove('active');
|
||||
document.getElementById('modelUsageBtn').classList.add('active');
|
||||
document.getElementById('apiUsageTable').classList.remove('active');
|
||||
document.getElementById('modelUsageTable').classList.add('active');
|
||||
// 添加动画效果
|
||||
document.getElementById('modelUsageTable').style.animation = 'none';
|
||||
setTimeout(() => {
|
||||
document.getElementById('modelUsageTable').style.animation = 'fadeIn 0.5s ease-in-out';
|
||||
}, 10);
|
||||
// 加载模型使用情况数据
|
||||
try {
|
||||
const response = await fetch(`${BASE_URL}/model-usage`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const modelUsageData = await response.json();
|
||||
const modelUsageTable = document.getElementById('modelUsageTable');
|
||||
|
||||
modelUsageTable.innerHTML = `
|
||||
<table class="rate-limit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${i18n("modelName")}</th>
|
||||
<th>${i18n("lastCallTime")}</th>
|
||||
<th>${i18n("usage")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${Object.entries(modelUsageData).map(([model, usage]) => `
|
||||
<tr>
|
||||
<td>${model}</td>
|
||||
<td>${usage.last_called_at ? new Date(usage.last_called_at).toLocaleString() : '未使用'}</td>
|
||||
<td>${usage.calls} token</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
} else {
|
||||
console.error(i18n('loadModelUsageFailed'), response.status, response.statusText);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(i18n('loadModelUsageError'), error);
|
||||
document.getElementById('modelUsageTable').innerHTML = `
|
||||
<p>${i18n('loadModelUsageFailed')}</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化用户中心
|
||||
function initUserCenter() {
|
||||
const navButtons = document.querySelectorAll('.user-center-nav-btn');
|
||||
navButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
const target = button.getAttribute('data-target');
|
||||
showUserCenterSection(target);
|
||||
});
|
||||
});
|
||||
|
||||
// 加载用户信息并更新头像
|
||||
fetch(`${BASE_URL}/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('access_token')}`
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(userData => {
|
||||
updateAllAvatars(userData.avatar);
|
||||
if (!userData.email_verified) {
|
||||
const sendVerificationEmailBtn = document.getElementById('sendVerificationEmail');
|
||||
const submitVerificationCodeBtn = document.getElementById('submitVerificationCode');
|
||||
if (sendVerificationEmailBtn) {
|
||||
sendVerificationEmailBtn.addEventListener('click', sendVerificationEmail);
|
||||
}
|
||||
if (submitVerificationCodeBtn) {
|
||||
submitVerificationCodeBtn.addEventListener('click', submitVerificationCode);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => console.error(i18n('getUserInfoFailed'), error));
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化用户中心
|
||||
window.addEventListener('load', initUserCenter);
|
||||
|
||||
// 将 showUserCenterSection 函数添加到全局作用域
|
||||
window.showUserCenterSection = showUserCenterSection;
|
||||
window.generateNewApiKey = generateNewApiKey;
|
||||
window.deleteApiKey = deleteApiKey;
|
||||
window.copyToClipboard = copyToClipboard;
|
||||
window.refreshApiKeys = refreshApiKeys;
|
||||
window.loadapiUsage = loadapiUsage;
|
||||
window.showApiUsage = showApiUsage;
|
||||
window.showModelUsage = showModelUsage;
|
||||
window.toggleApiKeyVisibility = toggleApiKeyVisibility;
|
||||
window.openAvatarModal = openAvatarModal;
|
||||
window.selectAvatar = selectAvatar;
|
||||
window.closeAvatarModal = closeAvatarModal;
|
||||
window.showUserCenterSection = showUserCenterSection;
|
||||
|
||||
|
||||
window.sendPhoneVerification = sendPhoneVerification;
|
||||
window.submitPhoneVerificationCode = submitPhoneVerificationCode;
|
||||
window.sendVerificationEmail = sendVerificationEmail;
|
||||
window.submitVerificationCode = submitVerificationCode;
|
||||
|
||||
|
||||
// 获取用户信息的函数
|
||||
async function fetchUserInfo() {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
console.error(i18n('noAccessTokenFound'));
|
||||
return null;
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(i18n('failedToFetchUserInfo'));
|
||||
return null;
|
||||
}
|
||||
|
||||
const userInfo = await response.json();
|
||||
return userInfo;
|
||||
|
||||
} catch (error) {
|
||||
console.error(i18n('errorFetchingUserInfo'), error);
|
||||
// 可以在这里处理错误,比如清除 token 并重定向到登录页面
|
||||
localStorage.removeItem('access_token');
|
||||
window.location.href = 'login.html'; // 重定向到登录页面
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// 显示API文档
|
||||
|
||||
// 在文件开头添加这个函数
|
||||
function i18n(key) {
|
||||
return lang[selectedLang][key] || key;
|
||||
}
|
||||
|
||||
async function showApiDocs() { // 添加 async 关键字
|
||||
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;
|
||||
}
|
||||
|
||||
const dashboardContent = document.getElementById('dashboardContent');
|
||||
const usersContent = document.getElementById('usersContent');
|
||||
const chatContent = document.getElementById('chatContent');
|
||||
const featureContent = document.getElementById('featureContent');
|
||||
const userCenterContent = document.getElementById('userCenterContent');
|
||||
const apiDocsContent = document.getElementById('apiDocsContent'); // 新增这一行
|
||||
const pageTitle = document.getElementById('pageTitle');
|
||||
|
||||
if (dashboardContent) dashboardContent.style.display = 'none';
|
||||
if (usersContent) usersContent.style.display = 'none';
|
||||
if (chatContent) chatContent.style.display = 'none';
|
||||
if (featureContent) featureContent.style.display = 'none';
|
||||
if (userCenterContent) userCenterContent.style.display = 'none';
|
||||
if (apiDocsContent) apiDocsContent.style.display = 'block'; // 新增这一行
|
||||
|
||||
if (pageTitle) {
|
||||
pageTitle.innerText = i18n('apiDocs');
|
||||
} // 添加缺失的闭合大括号
|
||||
} catch (error) {
|
||||
console.error('Error in showApiDocs:', error); // 修改错误信息
|
||||
alert(i18n('getUserInfoFailed') + error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化API文档
|
||||
function initApiDocs() {
|
||||
const docLink = document.querySelector('a[href="#"][onclick="showApiDocs()"]');
|
||||
if (docLink) {
|
||||
docLink.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
showApiDocs();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化API文档
|
||||
window.addEventListener('DOMContentLoaded', initApiDocs);
|
||||
|
||||
// 将函数添加到全局作用域
|
||||
window.showApiDocs = showApiDocs;
|
||||
|
||||
// 获取用户信息的函数
|
||||
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(i18n('failedToFetchUserInfo'));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,828 @@
|
||||
// lang.js
|
||||
|
||||
const lang = {
|
||||
zh: {
|
||||
"experienceCenter": "体验中心",
|
||||
"userManagement": "用户管理",
|
||||
"userCenter": "用户中心",
|
||||
"systemSettings": "系统设置",
|
||||
"pageTitle": "黑盒智能开发者平台",
|
||||
"dashboard": "仪表盘",
|
||||
"profile": "个人资料",
|
||||
"apiKey": "API Key",
|
||||
"apiUsage": "模型调用",
|
||||
"docs": "文档",
|
||||
"logout": "退出账户",
|
||||
"search": "搜索...",
|
||||
"notifications": "通知",
|
||||
"addUser": "添加",
|
||||
"exportUserData": "导出",
|
||||
"id": "ID",
|
||||
"name": "姓名",
|
||||
"email": "邮箱",
|
||||
"role": "角色",
|
||||
"actions": "操作",
|
||||
"chatAssistant": "智能助手",
|
||||
"inputPlaceholder": "输入文字或点击麦克风按钮开始语音输入",
|
||||
"defaultVoice": "默认音色",
|
||||
"girlVoice": "女孩",
|
||||
"womanVoice": "年轻女性",
|
||||
"manVoice": "年轻男性",
|
||||
"leijun": "雷军",
|
||||
"dufu": "杜甫",
|
||||
"hejiong": "何炅",
|
||||
"mahuateng": "马化腾",
|
||||
"lidan": "李诞",
|
||||
"yuhua": "余华",
|
||||
"liuzhenyun": "刘震云",
|
||||
"dabing": "大冰",
|
||||
"luoxiang": "罗翔",
|
||||
"xuzhiyuan": "许知远",
|
||||
"poseAnalysis": "姿态分析",
|
||||
"fallDetection": "跌倒检测",
|
||||
"objectDetection": "目标检测",
|
||||
"faceDetection": "人脸检测",
|
||||
"compare": "面部特征提取",
|
||||
"mediapipeFaceRecognition": "mediapipe面部识别",
|
||||
"miniCPMContentAnalysis": "MiniCPM内容分析",
|
||||
"qwenContentAnalysis": "Qwen内容分析",
|
||||
"miniCPMAnalysis": "MiniCPM OCR 分析",
|
||||
"qwenAnalysis": "Qwen OCR 分析",
|
||||
"inputImageUrl": "输入图片URL",
|
||||
"selectFile": "选择文件",
|
||||
"inputApiKey": "输入API Key",
|
||||
"takePicture": "拍照",
|
||||
"recordVideo": "录制视频",
|
||||
"stopRecording": "停止录制",
|
||||
"clear": "清除",
|
||||
"pleaseInputImageUrl": "请输入图片URL或上传图片",
|
||||
"loading": "加载中...",
|
||||
"detectionResult": "检测结果",
|
||||
"settings": "设置",
|
||||
"updateProfile": "更新个人资料",
|
||||
"generateApiKey": "生成新的 API 密钥",
|
||||
"copyApiKey": "复制 API 密钥",
|
||||
"deleteApiKey": "删除 API 密钥",
|
||||
"apiKeyStatus": "API 密钥状态",
|
||||
"active": "激活",
|
||||
"inactive": "未激活",
|
||||
"usageStatistics": "使用统计",
|
||||
"totalCalls": "总调用次数",
|
||||
"remainingCalls": "剩余调用次数",
|
||||
"apiDocs": "API 文档",
|
||||
"quickStart": "快速开始",
|
||||
"welcomeApi": "欢迎使用我们的API。本文档将指导您如何快速开始使用我们的服务。",
|
||||
"baseUrl": "基础URL",
|
||||
"authentication": "认证",
|
||||
"authDescription": "所有API请求都需要使用API密钥进行认证。您可以在用户中心的API密钥页面获取您的API密钥。",
|
||||
"sampleCode": "示例代码",
|
||||
"pythonCode": "Python",
|
||||
"replaceApiKey": "替换为您的 API 密钥",
|
||||
"replaceFilePath": "替换为您的文件路径",
|
||||
"replaceModelName": "替换为模型名称",
|
||||
"annotatedImageSaved": "标注图片已保存为",
|
||||
"taskFailed": "任务失败:",
|
||||
"unknownError": "未知错误",
|
||||
"timeoutMessage": "等待超时,请稍后手动检查结果",
|
||||
"pleaseLogin": "请登录以使用智能助手。",
|
||||
"pleaseVerifyEmail": "请先完成邮箱验证。",
|
||||
"errorGettingUserInfo": "获取用户信息时出错。请重试。",
|
||||
"tokenUsage": "Token 使用情况:",
|
||||
"pleaseSelectFile": "请选择一个文件!",
|
||||
"pleaseEnterApiKey": "请输入 API Key!",
|
||||
"errorUploadingFile": "上传文件时出错",
|
||||
"processingFailed": "处理失败",
|
||||
"errorGettingResults": "获取结果时出错",
|
||||
"resultImageNotFound": "结果图片不存在或尚未生成",
|
||||
"errorGettingResultImage": "获取结果图片时出错",
|
||||
"cameraAccessFailed": "摄像头访问失败: ",
|
||||
"cameraPermissionDenied": "无法访问摄像头,请确保已授予权限。",
|
||||
"featureDemo": "功能展示",
|
||||
"totalUsers": "总用户数",
|
||||
"newUsersToday": "今日新增用户",
|
||||
"activeUsers": "活跃用户",
|
||||
"recentNewUsers": "最近新增用户",
|
||||
"newUser": "新用户",
|
||||
"justNow": "刚刚",
|
||||
"minutesAgo": "分钟前",
|
||||
"hoursAgo": "小时前",
|
||||
"daysAgo": "天前",
|
||||
"numberOfNewUsers": "新增用户数量",
|
||||
"date": "日期",
|
||||
"updateUserFailed": "更新用户失败",
|
||||
"userInfoUpdateSuccess": "用户信息更新成功",
|
||||
"updateUserError": "更新用户时出错:",
|
||||
"updateUserFailed": "更新用户失败。请重试。",
|
||||
"confirmDeleteUser": "确定要删除这个用户吗?",
|
||||
"deleteUserFailed": "删除用户失败",
|
||||
"userDeleteSuccess": "用户删除成功",
|
||||
"deleteUserError": "删除用户时出错:",
|
||||
"deleteUserFailed": "删除用户失败。请重试。",
|
||||
"noUserDataToExport": "没有用户数据可导出",
|
||||
"addUserFailed": "添加用户失败",
|
||||
"addUserSuccess": "用户添加成功",
|
||||
"addUserError": "添加用户时出错:",
|
||||
"addUserFailed": "添加用户失败。请重试。",
|
||||
"fetchDashboardDataFailed": "获取仪表板数据失败。请重试。",
|
||||
"noRecentUsersData": "没有最近用户数据或格式无效",
|
||||
"noRecentUserActivities": "没有最近的用户活动",
|
||||
"activityListNotFound": "未找到活动列表元素",
|
||||
"pleaseLoginToViewUserList": "请登录以查看用户列表。",
|
||||
"pleaseCompleteEmailVerification": "请先完成邮箱验证。",
|
||||
"logoutError": "登出时发生错误:",
|
||||
"pleaseLoginToViewDashboard": "请登录以查看仪表盘。",
|
||||
"enterNewUsername": "请输入新的用户名:",
|
||||
"enterNewEmail": "请输入新的邮箱:",
|
||||
"enterNewPhoneNumber": "请输入新的手机号码:",
|
||||
"enterNewPassword": "请输入新的密码:",
|
||||
"allFieldsRequired": "所有字段都是必填的",
|
||||
"basicInfo": "基本资料",
|
||||
"username": "用户名",
|
||||
"usernameDescription": "您的用户名将显示在您的贡献或被提及的地方。",
|
||||
"emailDescription": "您可以在邮箱设置中管理验证过的邮箱地址。",
|
||||
"phoneNumber": "手机号",
|
||||
"updateProfile": "更新资料",
|
||||
"editAvatar": "编辑",
|
||||
"userVerification": "用户验证",
|
||||
"emailVerification": "邮箱验证:",
|
||||
"verified": "已验证",
|
||||
"unverified": "未验证",
|
||||
"sendVerificationEmail": "发送验证邮件",
|
||||
"enterVerificationCode": "输入验证码",
|
||||
"submitVerificationCode": "提交验证码",
|
||||
"phoneVerification": "手机验证:",
|
||||
"sendPhoneVerification": "发送验证短信",
|
||||
"enterPhoneVerificationCode": "输入验证码",
|
||||
"submitPhoneVerificationCode": "提交验证码",
|
||||
"selectAvatar": "选择头像",
|
||||
"close": "关闭",
|
||||
"apiKeyTable": "API密钥(点击复制)",
|
||||
"createdAt": "创建时间",
|
||||
"expiresAt": "过期时间",
|
||||
"createNewApiKey": "创建新密钥",
|
||||
"refreshList": "刷新",
|
||||
"noUsageRecord": "暂无使用记录",
|
||||
"tokensUsed": "已使用量",
|
||||
"totalTokens": "总使用量",
|
||||
"lastUsedAt": "最后使用时间",
|
||||
"unused": "未使用",
|
||||
"apiUsage": "API 使用情况",
|
||||
"modelUsage": "模型调用情况",
|
||||
"personalInfo": "个人资料",
|
||||
"apiKeys": "API 密钥",
|
||||
"modelCalls": "模型调用",
|
||||
"userCenter": "用户中心",
|
||||
"loadProfileFailed": "加载用户资料失败",
|
||||
"loadProfileError": "加载用户资料时出错:",
|
||||
"verificationEmailSent": "验证邮件已发送,请查收您的邮箱。",
|
||||
"resend": "重新发送",
|
||||
"sendVerificationEmailFailed": "发送验证邮件失败: ",
|
||||
"sendVerificationEmailError": "发送验证邮件时发生错误,请稍后重试。",
|
||||
"emailVerificationSuccess": "邮箱验证成功!",
|
||||
"verificationFailed": "验证失败: ",
|
||||
"submitVerificationCodeError": "提交验证码时发生错误,请稍后重试。",
|
||||
"verificationSMSSent": "验证短信已发送,请查收您的手机。",
|
||||
"sendVerificationSMSFailed": "发送验证短信失败: ",
|
||||
"sendVerificationSMSError": "发送验证短信时发生错误,请稍后重试。",
|
||||
"phoneVerificationSuccess": "手机验证成功!",
|
||||
"submitPhoneVerificationCodeError": "提交手机验证码时发生错误,请稍后重试。",
|
||||
"loadAvatarOptionsError": "加载头像选项时出错:",
|
||||
"avatarUpdateSuccess": "头像更新成功",
|
||||
"avatarUpdateFailed": "头像更新失败:",
|
||||
"avatarUpdateError": "更新头像时发生错误,请稍后重试",
|
||||
"profileUpdateSuccess": "个人资料更新成功",
|
||||
"profileUpdateFailed": "更新失败: ",
|
||||
"profileUpdateError": "更新失败,请重试",
|
||||
"invalidApiKey": "无效的 API 密钥",
|
||||
"loadApiKeysFailed": "加载API密钥失败,请稍后重试。",
|
||||
"loadApiKeysError": "加载API密钥时发生错误,请稍后重试。",
|
||||
"apiKeyCopied": "API密钥已复制到剪贴板",
|
||||
"copyTextFailed": "无法复制文本: ",
|
||||
"newApiKeyGenerated": "新的 API 密钥已生成: ",
|
||||
"expirationTime": "过期时间: ",
|
||||
"generateNewApiKeyFailed": "生成新的 API 密钥失败,请重试",
|
||||
"generateNewApiKeyError": "生成新API密钥时出错:",
|
||||
"confirmDeleteApiKey": "确定要删除这个API密钥吗?",
|
||||
"deleteApiKeyFailed": "删除API密钥失败,请重试",
|
||||
"deleteApiKeyError": "删除API密钥时出错:",
|
||||
"cacheCleared": "缓存已清除",
|
||||
"clearCacheFailed": "清除缓存失败:",
|
||||
"apiKeyListRefreshed": "API密钥列表已刷新",
|
||||
"loadModelCallInfoFailed": "加载模型调用信息失败",
|
||||
"loadModelCallInfoError": "加载模型调用信息时发生错误,请稍后重试。",
|
||||
"reload": "重新加载",
|
||||
"modelName": "模型名称",
|
||||
"lastCallTime": "最后调用时间",
|
||||
"usage": "使用量",
|
||||
"unused": "未使用",
|
||||
"loadModelUsageFailed": "加载模型使用情况失败",
|
||||
"loadModelUsageError": "加载模型使用情况时发生错误,请稍后重试。",
|
||||
"getUserInfoFailed": "获取用户信息失败:",
|
||||
"operation": "操作",
|
||||
"noAccessTokenFound": "No access token found",
|
||||
"failedToFetchUserInfo": "Failed to fetch user info:",
|
||||
"submittingVerificationCode": "正在提交验证码:",
|
||||
"delete": "删除",
|
||||
"pleaseEnterApiKey": "请输入您的API密钥:",
|
||||
"apiKeyRequired": "需要API密钥才能使用智能助手。",
|
||||
"recordingInProgress": "正在录音...",
|
||||
"recordingStopped": "录音已停止,正在处理...",
|
||||
"uploadFailed": "上传失败",
|
||||
"processingTranscription": "收到转录结果,正在处理...",
|
||||
"generatingAIResponse": "正在生成AI回答...",
|
||||
"processingComplete": "处理完成",
|
||||
"errorProcessingResponse": "处理回答时出错: ",
|
||||
"generatingSpeech": "语音生成中...",
|
||||
"speechGenerationComplete": "语音生成完成",
|
||||
"errorFetchingOrPlayingAudio": "获取或播放音频时出错: ",
|
||||
"ttsProcessingFailed": "TTS处理失败",
|
||||
"ttsProcessing": "TTS处理中... ({status})",
|
||||
"ttsPollingError": "获取TTS结果时出错",
|
||||
"speechGenerationComplete": "语音生成完成,正在播放音频",
|
||||
"errorFetchingOrPlayingAudio": "获取或播放音频时出错: ",
|
||||
"receivedChatPollResponse": "收到聊天轮询响应:",
|
||||
"invalidChatResponse": "聊天响应格式无效",
|
||||
"chatProcessingFailed": "聊天处理失败",
|
||||
"chatProcessing": "聊天处理中... ({status})",
|
||||
"chatPollingError": "轮询聊天结果时出错:",
|
||||
"getChatResultError": "获取聊天结果时出错: ",
|
||||
"uploadSuccess": "上传成功:",
|
||||
"uploadFailed": "上传失败:",
|
||||
"asrProcessingTimeout": "ASR处理超时",
|
||||
"pollingAsrResult": "正在获取ASR结果,任务: {taskId}, 尝试次数: {attempts}",
|
||||
"receivedAsrPollResponse": "收到ASR轮询响应:",
|
||||
"asrCompleted": "ASR完成,转录结果:",
|
||||
"asrProcessingFailed": "ASR处理失败",
|
||||
"asrTaskNotFound": "ASR任务未找到",
|
||||
"asrProcessing": "ASR处理中... ({status})",
|
||||
"pollingAsrResultError": "轮询ASR结果时出错:",
|
||||
"getAsrResultError": "获取ASR结果时出错",
|
||||
"apiKeyNotSet": "API密钥未设置",
|
||||
"initializingChat": "正在初始化对话...",
|
||||
"serverError": "服务器错误:",
|
||||
"chatInitialized": "对话已初始化,请开始输入",
|
||||
"errorInitializingChat": "初始化对话时出错: ",
|
||||
"startConversation": "开始对话",
|
||||
"errorAccessingMicrophone": "无法访问麦克风",
|
||||
"initializationResponse": "初始化响应:",
|
||||
"welcomeMessage": "你好!我是你的智能语音助手小Box。",
|
||||
"save":"保存",
|
||||
"cancel":"取消",
|
||||
"editUser":"编辑用户信息",
|
||||
"addNewUser": "添加新用户",
|
||||
"password": "密码",
|
||||
"phoneNumber": "手机号码",
|
||||
"userRole": "普通用户",
|
||||
"adminRole": "管理员",
|
||||
'searchPlaceholder': '搜索用户名、邮箱或角色(admin/user)...'
|
||||
},
|
||||
en: {
|
||||
"experienceCenter": "Experience",
|
||||
"userManagement": "Management",
|
||||
"userCenter": "UserCenter",
|
||||
"systemSettings": "Settings",
|
||||
"pageTitle": "Black Box AI Developer Platform",
|
||||
"dashboard": "Dashboard",
|
||||
"profile": "Profile",
|
||||
"apiKey": "API Key",
|
||||
"apiUsage": "API Usage",
|
||||
"docs": "Documentation",
|
||||
"logout": "Logout",
|
||||
"search": "Search...",
|
||||
"notifications": "Notifications",
|
||||
"addUser": "Add",
|
||||
"exportUserData": "Export",
|
||||
"id": "ID",
|
||||
"name": "Name",
|
||||
"email": "Email",
|
||||
"role": "Role",
|
||||
"actions": "Actions",
|
||||
"chatAssistant": "Chat Assistant",
|
||||
"inputPlaceholder": "Type a message or click the microphone to start voice input",
|
||||
"defaultVoice": "Default Voice",
|
||||
"girlVoice": "Girl",
|
||||
"womanVoice": "Woman",
|
||||
"manVoice": "Man",
|
||||
"leijun": "Lei Jun",
|
||||
"dufu": "Du Fu",
|
||||
"hejiong": "He Jiong",
|
||||
"mahuateng": "Ma Huateng",
|
||||
"lidan": "Li Dan",
|
||||
"yuhua": "Yu Hua",
|
||||
"liuzhenyun": "Liu Zhenyun",
|
||||
"dabing": "Da Bing",
|
||||
"luoxiang": "Luo Xiang",
|
||||
"xuzhiyuan": "Xu Zhiyuan",
|
||||
"poseAnalysis": "Pose Analysis",
|
||||
"fallDetection": "Fall Detection",
|
||||
"objectDetection": "Object Detection",
|
||||
"faceDetection": "Face Detection",
|
||||
"compare": "Face Feature Detection",
|
||||
"mediapipeFaceRecognition": "Mediapipe Face Recognition",
|
||||
"miniCPMContentAnalysis": "MiniCPM Content Analysis",
|
||||
"miniCPMAnalysis": "MiniCPM OCR Analysis",
|
||||
"qwenAnalysis": "Qwen OCR Analysis",
|
||||
"qwenContentAnalysis": "Qwen Content Analysis",
|
||||
"inputImageUrl": "Input Image URL",
|
||||
"selectFile": "Select File",
|
||||
"inputApiKey": "Input API Key",
|
||||
"takePicture": "Take Picture",
|
||||
"recordVideo": "Record Video",
|
||||
"stopRecording": "Stop Recording",
|
||||
"clear": "Clear",
|
||||
"pleaseInputImageUrl": "Please input image URL or upload an image",
|
||||
"loading": "Loading...",
|
||||
"detectionResult": "Detection Result",
|
||||
"settings": "Settings",
|
||||
"updateProfile": "Update Profile",
|
||||
"generateApiKey": "Generate New API Key",
|
||||
"copyApiKey": "Copy API Key",
|
||||
"deleteApiKey": "Delete API Key",
|
||||
"apiKeyStatus": "API Key Status",
|
||||
"active": "Active",
|
||||
"inactive": "Inactive",
|
||||
"usageStatistics": "Usage Statistics",
|
||||
"totalCalls": "Total Calls",
|
||||
"remainingCalls": "Remaining Calls",
|
||||
"apiDocs": "API Documentation",
|
||||
"quickStart": "Quick Start",
|
||||
"welcomeApi": "Welcome to our API. This document will guide you on how to quickly start using our services.",
|
||||
"baseUrl": "Base URL",
|
||||
"authentication": "Authentication",
|
||||
"authDescription": "All API requests require authentication using an API key. You can obtain your API key from the API key page in the user center.",
|
||||
"sampleCode": "Sample Code",
|
||||
"pythonCode": "Python",
|
||||
"replaceApiKey": "Replace with your API key",
|
||||
"replaceFilePath": "Replace with your file path",
|
||||
"replaceModelName": "Replace with model name",
|
||||
"annotatedImageSaved": "Annotated image has been saved as",
|
||||
"taskFailed": "Task failed:",
|
||||
"unknownError": "Unknown error",
|
||||
"timeoutMessage": "Timeout waiting, please check the result manually later",
|
||||
"pleaseLogin": "Please login to use the intelligent assistant.",
|
||||
"pleaseVerifyEmail": "Please complete email verification first.",
|
||||
"errorGettingUserInfo": "Error getting user information. Please try again.",
|
||||
"tokenUsage": "Token usage:",
|
||||
"pleaseSelectFile": "Please select a file!",
|
||||
"pleaseEnterApiKey": "Please enter API Key!",
|
||||
"errorUploadingFile": "Error uploading file",
|
||||
"processingFailed": "Processing failed",
|
||||
"errorGettingResults": "Error getting results",
|
||||
"resultImageNotFound": "Result image not found or not yet generated",
|
||||
"errorGettingResultImage": "Error getting result image",
|
||||
"cameraAccessFailed": "Camera access failed: ",
|
||||
"cameraPermissionDenied": "Unable to access camera. Please ensure permission is granted.",
|
||||
"featureDemo": "Feature Demo",
|
||||
"totalUsers": "Total Users",
|
||||
"newUsersToday": "New Users Today",
|
||||
"activeUsers": "Active Users",
|
||||
"recentNewUsers": "Recent New Users",
|
||||
"newUser": "New user",
|
||||
"justNow": "Just now",
|
||||
"minutesAgo": "minutes ago",
|
||||
"hoursAgo": "hours ago",
|
||||
"daysAgo": "days ago",
|
||||
"numberOfNewUsers": "Number of New Users",
|
||||
"date": "Date",
|
||||
"updateUserFailed": "Failed to update user",
|
||||
"userInfoUpdateSuccess": "User information updated successfully",
|
||||
"updateUserError": "Error updating user:",
|
||||
"updateUserFailed": "Failed to update user. Please try again.",
|
||||
"confirmDeleteUser": "Are you sure you want to delete this user?",
|
||||
"deleteUserFailed": "Failed to delete user",
|
||||
"userDeleteSuccess": "User deleted successfully",
|
||||
"deleteUserError": "Error deleting user:",
|
||||
"deleteUserFailed": "Failed to delete user. Please try again.",
|
||||
"noUserDataToExport": "No user data to export",
|
||||
"addUserFailed": "Failed to add user",
|
||||
"addUserSuccess": "User added successfully",
|
||||
"addUserError": "Error adding user:",
|
||||
"addUserFailed": "Failed to add user. Please try again.",
|
||||
"fetchDashboardDataFailed": "Failed to fetch dashboard data. Please try again.",
|
||||
"noRecentUsersData": "No recent users data or invalid format",
|
||||
"noRecentUserActivities": "No recent user activities available",
|
||||
"activityListNotFound": "Activity list element not found",
|
||||
"pleaseLoginToViewUserList": "Please login to view the user list.",
|
||||
"pleaseCompleteEmailVerification": "Please complete email verification first.",
|
||||
"logoutError": "Error occurred during logout:",
|
||||
"pleaseLoginToViewDashboard": "Please login to view the dashboard.",
|
||||
"enterNewUsername": "Please enter new username:",
|
||||
"enterNewEmail": "Please enter new email:",
|
||||
"enterNewPhoneNumber": "Please enter new phone number:",
|
||||
"enterNewPassword": "Please enter new password:",
|
||||
"allFieldsRequired": "All fields are required",
|
||||
"basicInfo": "Basic Information",
|
||||
"username": "Username",
|
||||
"usernameDescription": "Your username will be displayed where you contribute or are mentioned.",
|
||||
"emailDescription": "You can manage verified email addresses in email settings.",
|
||||
"phoneNumber": "Phone Number",
|
||||
"updateProfile": "Update Profile",
|
||||
"editAvatar": "Edit",
|
||||
"userVerification": "User Verification",
|
||||
"emailVerification": "Email Verification:",
|
||||
"verified": "Verified",
|
||||
"unverified": "Unverified",
|
||||
"sendVerificationEmail": "Send Verification Email",
|
||||
"enterVerificationCode": "Enter Verification Code",
|
||||
"submitVerificationCode": "Submit Verification Code",
|
||||
"phoneVerification": "Phone Verification:",
|
||||
"sendPhoneVerification": "Send Verification SMS",
|
||||
"enterPhoneVerificationCode": "Enter Verification Code",
|
||||
"submitPhoneVerificationCode": "Submit Verification Code",
|
||||
"selectAvatar": "Select Avatar",
|
||||
"close": "Close",
|
||||
"apiKeyTable": "API Key (Click to Copy)",
|
||||
"createdAt": "Created At",
|
||||
"expiresAt": "Expires At",
|
||||
"createNewApiKey": "Create New Key",
|
||||
"refreshList": "Refresh",
|
||||
"noUsageRecord": "No Usage Record",
|
||||
"tokensUsed": "Tokens Used",
|
||||
"totalTokens": "Total Tokens",
|
||||
"lastUsedAt": "Last Used At",
|
||||
"unused": "Unused",
|
||||
"apiUsage": "API Usage",
|
||||
"modelUsage": "Model Usage",
|
||||
"personalInfo": "Personal Information",
|
||||
"apiKeys": "API Keys",
|
||||
"modelCalls": "Model Calls",
|
||||
"userCenter": "User Center",
|
||||
"loadProfileFailed": "Failed to load user profile",
|
||||
"loadProfileError": "Error loading user profile: ",
|
||||
"verificationEmailSent": "Verification email has been sent. Please check your inbox.",
|
||||
"resend": "Resend",
|
||||
"sendVerificationEmailFailed": "Failed to send verification email: ",
|
||||
"sendVerificationEmailError": "An error occurred while sending the verification email. Please try again later.",
|
||||
"emailVerificationSuccess": "Email verification successful!",
|
||||
"verificationFailed": "Verification failed: ",
|
||||
"submitVerificationCodeError": "An error occurred while submitting the verification code. Please try again later.",
|
||||
"verificationSMSSent": "Verification SMS has been sent. Please check your phone.",
|
||||
"sendVerificationSMSFailed": "Failed to send verification SMS: ",
|
||||
"sendVerificationSMSError": "An error occurred while sending the verification SMS. Please try again later.",
|
||||
"phoneVerificationSuccess": "Phone verification successful!",
|
||||
"submitPhoneVerificationCodeError": "An error occurred while submitting the phone verification code. Please try again later.",
|
||||
"loadAvatarOptionsError": "Error loading avatar options: ",
|
||||
"avatarUpdateSuccess": "Avatar updated successfully",
|
||||
"avatarUpdateFailed": "Avatar update failed: ",
|
||||
"avatarUpdateError": "An error occurred while updating the avatar. Please try again later",
|
||||
"profileUpdateSuccess": "Profile updated successfully",
|
||||
"profileUpdateFailed": "Update failed: ",
|
||||
"profileUpdateError": "Update failed. Please try again",
|
||||
"invalidApiKey": "Invalid API key",
|
||||
"loadApiKeysFailed": "Failed to load API keys. Please try again later.",
|
||||
"loadApiKeysError": "An error occurred while loading API keys. Please try again later.",
|
||||
"apiKeyCopied": "API key copied to clipboard",
|
||||
"copyTextFailed": "Unable to copy text: ",
|
||||
"newApiKeyGenerated": "New API key generated: ",
|
||||
"expirationTime": "Expiration time: ",
|
||||
"generateNewApiKeyFailed": "Failed to generate new API key. Please try again",
|
||||
"generateNewApiKeyError": "Failed to generate new API key. Please try again",
|
||||
"confirmDeleteApiKey": "Are you sure you want to delete this API key?",
|
||||
"deleteApiKeyFailed": "Failed to delete API key. Please try again",
|
||||
"deleteApiKeyError": "Failed to delete API key. Please try again",
|
||||
"cacheCleared": "Cache cleared",
|
||||
"clearCacheFailed": "Failed to clear cache: ",
|
||||
"apiKeyListRefreshed": "API key list refreshed",
|
||||
"loadModelCallInfoFailed": "Failed to load model call information",
|
||||
"loadModelCallInfoError": "An error occurred while loading model call information. Please try again later.",
|
||||
"reload": "Reload",
|
||||
"modelName": "Model Name",
|
||||
"lastCallTime": "Last Call Time",
|
||||
"usage": "Usage",
|
||||
"unused": "Unused",
|
||||
"loadModelUsageFailed": "Failed to load model usage information",
|
||||
"loadModelUsageError": "An error occurred while loading model usage information. Please try again later.",
|
||||
"getUserInfoFailed": "Failed to get user information: ",
|
||||
"operation": "Operation",
|
||||
"noAccessTokenFound": "No access token found",
|
||||
"failedToFetchUserInfo": "Failed to fetch user info:",
|
||||
"submittingVerificationCode": "Submitting verification code:",
|
||||
"delete": "Delete",
|
||||
"pleaseEnterApiKey": "Please enter your API key:",
|
||||
"apiKeyRequired": "API key is required to use the intelligent assistant.",
|
||||
"recordingInProgress": "Recording in progress...",
|
||||
"recordingStopped": "Recording stopped, processing...",
|
||||
"uploadFailed": "Upload failed",
|
||||
"processingTranscription": "Received transcription result, processing...",
|
||||
"generatingAIResponse": "Generating AI response...",
|
||||
"processingComplete": "Processing complete",
|
||||
"errorProcessingResponse": "Error processing response: ",
|
||||
"generatingSpeech": "Generating speech...",
|
||||
"speechGenerationComplete": "Speech generation complete",
|
||||
"errorFetchingOrPlayingAudio": "Error fetching or playing audio: ",
|
||||
"ttsProcessingFailed": "TTS processing failed",
|
||||
"ttsProcessing": "TTS processing... ({status})",
|
||||
"ttsPollingError": "Error getting TTS results",
|
||||
"speechGenerationComplete": "Speech generation complete, playing audio",
|
||||
"errorFetchingOrPlayingAudio": "Error fetching or playing audio: ",
|
||||
"receivedChatPollResponse": "Received chat poll response:",
|
||||
"invalidChatResponse": "Invalid chat response format",
|
||||
"chatProcessingFailed": "Chat processing failed",
|
||||
"chatProcessing": "Chat processing... ({status})",
|
||||
"chatPollingError": "Error polling chat result:",
|
||||
"getChatResultError": "Error getting chat result: ",
|
||||
"uploadSuccess": "Upload successful:",
|
||||
"uploadFailed": "Upload failed:",
|
||||
"asrProcessingTimeout": "ASR processing timeout",
|
||||
"pollingAsrResult": "Polling ASR result for task: {taskId}, attempt: {attempts}",
|
||||
"receivedAsrPollResponse": "Received ASR poll response:",
|
||||
"asrCompleted": "ASR completed, transcription:",
|
||||
"asrProcessingFailed": "ASR processing failed",
|
||||
"asrTaskNotFound": "ASR task not found",
|
||||
"asrProcessing": "ASR processing... ({status})",
|
||||
"pollingAsrResultError": "Error polling ASR result:",
|
||||
"getAsrResultError": "Error getting ASR result",
|
||||
"apiKeyNotSet": "API key not set",
|
||||
"initializingChat": "Initializing chat...",
|
||||
"serverError": "Server error:",
|
||||
"chatInitialized": "Chat initialized, please start input",
|
||||
"errorInitializingChat": "Error initializing chat: ",
|
||||
"startConversation": "Start conversation",
|
||||
"errorAccessingMicrophone": "Unable to access microphone",
|
||||
"initializationResponse": "Initialization response:",
|
||||
"welcomeMessage": "Hello! I'm your intelligent voice assistant, Little Box.",
|
||||
"save":"save",
|
||||
"cancel":"cancel",
|
||||
"editUser":"Edit User Data",
|
||||
"addNewUser": "Add New User",
|
||||
"password": "Password",
|
||||
"phoneNumber": "Phone Number",
|
||||
"userRole": "User",
|
||||
"adminRole": "Admin",
|
||||
'searchPlaceholder': 'Search by name, email or role(admin/user)...'
|
||||
},
|
||||
ko: {
|
||||
"experienceCenter": "체험 센터",
|
||||
"userManagement": "사용자 관리",
|
||||
"userCenter": "사용자 센터",
|
||||
"systemSettings": "시스템 설정",
|
||||
"pageTitle": "블랙박스 AI 개발자 플랫폼",
|
||||
"dashboard": "대시보드",
|
||||
"profile": "프로필",
|
||||
"apiKey": "API Key",
|
||||
"apiUsage": "API 사용량",
|
||||
"docs": "문서",
|
||||
"logout": "로그아웃",
|
||||
"search": "검색...",
|
||||
"notifications": "알림",
|
||||
"addUser": "추가",
|
||||
"exportUserData": "내보내기",
|
||||
"id": "ID",
|
||||
"name": "이름",
|
||||
"email": "이메일",
|
||||
"role": "역할",
|
||||
"actions": "작업",
|
||||
"chatAssistant": "채팅 도우미",
|
||||
"inputPlaceholder": "메시지를 입력하거나 마이크를 클릭하여 음성 입력 시작",
|
||||
"defaultVoice": "기본 음성",
|
||||
"girlVoice": "소녀",
|
||||
"womanVoice": "여성",
|
||||
"manVoice": "남성",
|
||||
"leijun": "레이쥔",
|
||||
"dufu": "두보",
|
||||
"hejiong": "허중",
|
||||
"mahuateng": "마화텅",
|
||||
"lidan": "리단",
|
||||
"yuhua": "위화",
|
||||
"liuzhenyun": "류전윈",
|
||||
"dabing": "다빙",
|
||||
"luoxiang": "뤄샹",
|
||||
"xuzhiyuan": "쉬즈위안",
|
||||
"poseAnalysis": "자세 분석",
|
||||
"fallDetection": "낙상 감지",
|
||||
"objectDetection": "객체 감지",
|
||||
"faceDetection": "얼굴 감지",
|
||||
"compare": "얼굴 특징 감지",
|
||||
"mediapipeFaceRecognition": "Mediapipe 얼굴 인식",
|
||||
"miniCPMContentAnalysis": "MiniCPM 콘텐츠 분석",
|
||||
"qwenContentAnalysis": "Qwen 콘텐츠 분석",
|
||||
"miniCPMAnalysis": "MiniCPM OCR 분석",
|
||||
"qwenAnalysis": "Qwen OCR 분석",
|
||||
"inputImageUrl": "이미지 URL 입력",
|
||||
"selectFile": "파일 선택",
|
||||
"inputApiKey": "API 키 입력",
|
||||
"takePicture": "사진 찍기",
|
||||
"recordVideo": "비디오 녹화",
|
||||
"stopRecording": "녹화 중지",
|
||||
"clear": "지우기",
|
||||
"pleaseInputImageUrl": "이미지 URL을 입력하거나 이미지를 업로드하세요",
|
||||
"loading": "로딩 중...",
|
||||
"detectionResult": "감지 결과",
|
||||
"settings": "설정",
|
||||
"updateProfile": "프로필 업데이트",
|
||||
"generateApiKey": "새 API 키 생성",
|
||||
"copyApiKey": "API 키 복사",
|
||||
"deleteApiKey": "API 키 삭제",
|
||||
"apiKeyStatus": "API 키 상태",
|
||||
"active": "활성",
|
||||
"inactive": "비활성",
|
||||
"usageStatistics": "사용 통계",
|
||||
"totalCalls": "총 호출 수",
|
||||
"remainingCalls": "남은 호출 수",
|
||||
"apiDocs": "API 문서",
|
||||
"quickStart": "빠른 시작",
|
||||
"welcomeApi": "API를 사용해 주셔서 감사합니다. 이 문서는 서비스 사용을 빠르게 시작하는 방법을 안내합니다.",
|
||||
"baseUrl": "기본 URL",
|
||||
"authentication": "인증",
|
||||
"authDescription": "모든 API 요청은 API 키를 사용한 인증이 필요합니다. 사용자 센터의 API 키 페이지에서 API 키를 얻을 수 있습니다.",
|
||||
"sampleCode": "샘플 코드",
|
||||
"pythonCode": "Python",
|
||||
"replaceApiKey": "API 키로 교체하세요",
|
||||
"replaceFilePath": "파일 경로로 교체하세요",
|
||||
"replaceModelName": "모델 이름으로 교체하세요",
|
||||
"annotatedImageSaved": "주석이 달린 이미지가 다음과 같이 저장되었습니다",
|
||||
"taskFailed": "작업 실패:",
|
||||
"unknownError": "알 수 없는 오류",
|
||||
"timeoutMessage": "대기 시간 초과, 나중에 수동으로 결과를 확인해 주세요",
|
||||
"pleaseLogin": "지능형 어시스턴트를 사용하려면 로그인하세요.",
|
||||
"pleaseVerifyEmail": "먼저 이메일 인증을 완료해주세요.",
|
||||
"errorGettingUserInfo": "사용자 정보를 가져오는 중 오류가 발생했습니다. 다시 시도해주세요.",
|
||||
"tokenUsage": "토큰 사용량:",
|
||||
"pleaseSelectFile": "파일을 선택해주세요!",
|
||||
"pleaseEnterApiKey": "API 키를 입력해주세요!",
|
||||
"errorUploadingFile": "파일 업로드 중 오류 발생",
|
||||
"processingFailed": "처리 실패",
|
||||
"errorGettingResults": "결과를 가져오는 중 오류 발생",
|
||||
"resultImageNotFound": "결과 이미지를 찾을 수 없거나 아직 생성되지 않았습니다",
|
||||
"errorGettingResultImage": "결과 이미지를 가져오는 중 오류 발생",
|
||||
"cameraAccessFailed": "카메라 접근 실패: ",
|
||||
"cameraPermissionDenied": "카메라에 접근할 수 없습니다. 권한이 부여되었는지 확인해주세요.",
|
||||
"featureDemo": "기능 데모",
|
||||
"totalUsers": "총 사용자 수",
|
||||
"newUsersToday": "오늘의 신규 사용자",
|
||||
"activeUsers": "활성 사용자",
|
||||
"recentNewUsers": "최근 신규 사용자",
|
||||
"newUser": "신규 사용자",
|
||||
"justNow": "방금",
|
||||
"minutesAgo": "분 전",
|
||||
"hoursAgo": "시간 전",
|
||||
"daysAgo": "일 전",
|
||||
"numberOfNewUsers": "신규 사용자 수",
|
||||
"date": "날짜",
|
||||
"updateUserFailed": "사용자 업데이트 실패",
|
||||
"userInfoUpdateSuccess": "사용자 정보가 성공적으로 업데이트되었습니다",
|
||||
"updateUserError": "사용자 업데이트 중 오류 발생:",
|
||||
"updateUserFailed": "사용자 업데이트에 실패했습니다. 다시 시도해주세요.",
|
||||
"confirmDeleteUser": "이 사용자를 삭제하시겠습니까?",
|
||||
"deleteUserFailed": "사용자 삭제 실패",
|
||||
"userDeleteSuccess": "사용자가 성공적으로 삭제되었습니다",
|
||||
"deleteUserError": "사용자 삭제 중 오류 발생:",
|
||||
"deleteUserFailed": "사용자 삭제에 실패했습니다. 다시 시도해주세요.",
|
||||
"noUserDataToExport": "내보낼 사용자 데이터가 없습니다",
|
||||
"addUserFailed": "사용자 추가 실패",
|
||||
"addUserSuccess": "사용자가 성공적으로 추가되었습니다",
|
||||
"addUserError": "사용자 추가 중 오류 발생:",
|
||||
"addUserFailed": "사용자 추가에 실패했습니다. 다시 시도해주세요.",
|
||||
"fetchDashboardDataFailed": "대시보드 데이터 가져오기 실패. 다시 시도해주세요.",
|
||||
"noRecentUsersData": "최근 사용자 데이터가 없거나 형식이 잘못되었습니다",
|
||||
"noRecentUserActivities": "최근 사용자 활동이 없습니다",
|
||||
"activityListNotFound": "활동 목록 요소를 찾을 수 없습니다",
|
||||
"pleaseLoginToViewUserList": "사용자 목록을 보려면 로그인하세요.",
|
||||
"pleaseCompleteEmailVerification": "먼저 이메일 인증을 완료해주세요.",
|
||||
"logoutError": "로그아웃 중 오류 발생:",
|
||||
"pleaseLoginToViewDashboard": "대시보드를 보려면 로그인하세요.",
|
||||
"enterNewUsername": "새 사용자 이름을 입력하세요:",
|
||||
"enterNewEmail": "새 이메일을 입력하세요:",
|
||||
"enterNewPhoneNumber": "새 전화번호를 입력하세요:",
|
||||
"enterNewPassword": "새 비밀번호를 입력하세요:",
|
||||
"allFieldsRequired": "모든 필드를 입력해야 합니다",
|
||||
"basicInfo": "기본 정보",
|
||||
"username": "사용자 이름",
|
||||
"usernameDescription": "사용자 이름은 귀하의 기여나 언급된 곳에 표시됩니다.",
|
||||
"emailDescription": "이메일 설정에서 인증된 이메일 주소를 관리할 수 있습니다.",
|
||||
"phoneNumber": "전화번호",
|
||||
"editAvatar": "편집",
|
||||
"userVerification": "사용자 인증",
|
||||
"emailVerification": "이메일 인증:",
|
||||
"verified": "인증됨",
|
||||
"unverified": "미인증",
|
||||
"sendVerificationEmail": "인증 이메일 보내기",
|
||||
"enterVerificationCode": "인증 코드 입력",
|
||||
"submitVerificationCode": "인증 코드 제출",
|
||||
"phoneVerification": "전화번호 인증:",
|
||||
"sendPhoneVerification": "인증 SMS 보내기",
|
||||
"enterPhoneVerificationCode": "인증 코드 입력",
|
||||
"submitPhoneVerificationCode": "인증 코드 제출",
|
||||
"selectAvatar": "아바타 선택",
|
||||
"close": "닫기",
|
||||
"apiKeyTable": "API Key (클릭하여 복사)",
|
||||
"createdAt": "생성 시간",
|
||||
"expiresAt": "만료 시간",
|
||||
"createNewApiKey": "새 키 생성",
|
||||
"refreshList": "새로고침",
|
||||
"noUsageRecord": "새 키 만들기",
|
||||
"tokensUsed": "사용된 토큰",
|
||||
"totalTokens": "총 토큰",
|
||||
"lastUsedAt": "마지막 사용 시간",
|
||||
"unused": "미사용",
|
||||
"apiUsage": "API 사용 현황",
|
||||
"modelUsage": "모델 사용 현황",
|
||||
"personalInfo": "개인 정보",
|
||||
"apiKeys": "API Keys",
|
||||
"modelCalls": "모델 호출",
|
||||
"userCenter": "사용자 센터",
|
||||
"loadProfileFailed": "사용자 프로필 드 실패",
|
||||
"loadProfileError": "사용자 프로필 로드 중 오류 발생: ",
|
||||
"verificationEmailSent": "인증 이메일이 발송되었습니다. 받은 편지함을 확인해 주세요.",
|
||||
"resend": "재전송",
|
||||
"sendVerificationEmailFailed": "인증 이메일 발송 실패: ",
|
||||
"sendVerificationEmailError": "인증 이메일 발송 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
|
||||
"emailVerificationSuccess": "이메일 인증 성공!",
|
||||
"verificationFailed": "인증 실패: ",
|
||||
"submitVerificationCodeError": "인증 코드 제출 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
|
||||
"verificationSMSSent": "인증 SMS가 발송되었습니다. 휴대폰을 확인해 주세요.",
|
||||
"sendVerificationSMSFailed": "인증 SMS 발송 실패: ",
|
||||
"sendVerificationSMSError": "인증 SMS 발송 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
|
||||
"phoneVerificationSuccess": "전화번호 인증 성공!",
|
||||
"submitPhoneVerificationCodeError": "전화번호 인증 코드 제출 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
|
||||
"loadAvatarOptionsError": "아바타 옵션 로드 중 오류 발생: ",
|
||||
"avatarUpdateSuccess": "아바타 업데이트 성공",
|
||||
"avatarUpdateFailed": "아바타 업데이트 실패: ",
|
||||
"avatarUpdateError": "아바타 업데이트 중 오류가 발생했습니다. 나중에 다시 시도해 주세요",
|
||||
"profileUpdateSuccess": "프로필 업데이트 성공",
|
||||
"profileUpdateFailed": "업데이트 실패: ",
|
||||
"profileUpdateError": "업데이트 실패. 다시 시도해 주세요",
|
||||
"invalidApiKey": "유효하지 않은 API 키",
|
||||
"loadApiKeysFailed": "API 키 로드 실패. 나중에 다시 시도해 주세요.",
|
||||
"loadApiKeysError": "API 키 로드 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
|
||||
"apiKeyCopied": "API 키가 클립보드에 복사되었습니다",
|
||||
"copyTextFailed": "텍스트 복사 실패: ",
|
||||
"newApiKeyGenerated": "새 API 키 생성: ",
|
||||
"expirationTime": "만료 시간: ",
|
||||
"generateNewApiKeyFailed": "새 API 키 생성 실패. 다시 시도해 주세요",
|
||||
"generateNewApiKeyError": "새 API 키 생성 실패. 다시 시도해 주세요",
|
||||
"confirmDeleteApiKey": "이 API 키를 삭제하시겠습니까?",
|
||||
"deleteApiKeyFailed": "API 키 삭제 실패. 다시 시도해 주세요",
|
||||
"deleteApiKeyError": "API 키 삭제 실패. 다시 시도해 주세요",
|
||||
"cacheCleared": "캐시 삭제 완료",
|
||||
"clearCacheFailed": "캐시 삭제 실패: ",
|
||||
"apiKeyListRefreshed": "API 키 목록 새로고침 완료",
|
||||
"loadModelCallInfoFailed": "모델 호출 정보 로드 실패",
|
||||
"loadModelCallInfoError": "모델 호출 정보 로드 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
|
||||
"reload": "새로고침",
|
||||
"modelName": "모델 이름",
|
||||
"lastCallTime": "마지막 호출 시간",
|
||||
"usage": "사용량",
|
||||
"unused": "미사용",
|
||||
"loadModelUsageFailed": "모델 사용 정보 로드 실패",
|
||||
"loadModelUsageError": "모델 사용 정보 로드 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
|
||||
"getUserInfoFailed": "사용자 정보 가져오기 실패: ",
|
||||
"operation": "작업",
|
||||
"noAccessTokenFound": "액세스 토큰을 찾을 수 없습니다",
|
||||
"failedToFetchUserInfo": "사용자 정보를 가져오는데 실패했습니다:",
|
||||
"submittingVerificationCode": "인증 코드 제출 중:",
|
||||
"delete":"삭제",
|
||||
"pleaseEnterApiKey": "API 키를 입력해주세요:",
|
||||
"apiKeyRequired": "지능형 어시스턴트를 사용하려면 API 키가 필요합니다.",
|
||||
"recordingInProgress": "녹음 중...",
|
||||
"recordingStopped": "녹음이 중지되었습니다. 처리 중...",
|
||||
"uploadFailed": "업로드 실패",
|
||||
"processingTranscription": "전사 결과를 받았습니다. 처리 중...",
|
||||
"generatingAIResponse": "AI 응답 생성 중...",
|
||||
"processingComplete": "처리 완료",
|
||||
"errorProcessingResponse": "응답 처리 중 오류 발생: ",
|
||||
"generatingSpeech": "음성 생성 중...",
|
||||
"speechGenerationComplete": "음성 생성 완료",
|
||||
"errorFetchingOrPlayingAudio": "오디오 가져오기 또는 재생 중 오류 발생: ",
|
||||
"ttsProcessingFailed": "TTS 처리 실패",
|
||||
"ttsProcessing": "TTS 처리 중... ({status})",
|
||||
"ttsPollingError": "TTS 결과 가져오기 오류",
|
||||
"speechGenerationComplete": "음성 생성 완료, 오디오 재생 중",
|
||||
"errorFetchingOrPlayingAudio": "오디오 가져오기 또는 재생 중 오류 발생: ",
|
||||
"receivedChatPollResponse": "채팅 폴링 응답 수신:",
|
||||
"invalidChatResponse": "채팅 응답 형식이 잘못되었습니다",
|
||||
"chatProcessingFailed": "채팅 처리 실패",
|
||||
"chatProcessing": "채팅 처리 중... ({status})",
|
||||
"chatPollingError": "채팅 결과 폴링 중 오류 발생:",
|
||||
"getChatResultError": "채팅 결과를 가져오는 중 오류 발생: ",
|
||||
"uploadSuccess": "업로드 성공:",
|
||||
"uploadFailed": "업로드 실패:",
|
||||
"asrProcessingTimeout": "ASR 처리 시간 초과",
|
||||
"pollingAsrResult": "ASR 결과 폴링 중, 작업: {taskId}, 시도 횟수: {attempts}",
|
||||
"receivedAsrPollResponse": "ASR 폴링 응답 수신:",
|
||||
"asrCompleted": "ASR 완료, 전사 결과:",
|
||||
"asrProcessingFailed": "ASR 처리 실패",
|
||||
"asrTaskNotFound": "ASR 작업을 찾을 수 없음",
|
||||
"asrProcessing": "ASR 처리 중... ({status})",
|
||||
"pollingAsrResultError": "ASR 결과 폴링 중 오류 발생:",
|
||||
"getAsrResultError": "ASR 결과 가져오기 중 오류 발생",
|
||||
"apiKeyNotSet": "API 키가 설정되지 않았습니다",
|
||||
"initializingChat": "대화를 초기화하는 중...",
|
||||
"serverError": "서버 오류:",
|
||||
"chatInitialized": "대화가 초기화되었습니다. 입력을 시작하세요",
|
||||
"errorInitializingChat": "대화 초기화 중 오류 발생: ",
|
||||
"startConversation": "대화 시작",
|
||||
"errorAccessingMicrophone": "마이크에 접근할 수 없습니다",
|
||||
"initializationResponse": "초기화 응답:",
|
||||
"welcomeMessage": "안녕하세요! 저는 당신의 지능형 음성 비서 리틀 박스입니다.",
|
||||
"save":"저장",
|
||||
"cancel":"취소",
|
||||
"editUser":"사용자 데이터 편집",
|
||||
"addNewUser": "새 사용자 추가",
|
||||
"password": "비밀번호",
|
||||
"phoneNumber": "전화번호",
|
||||
"userRole": "일반 사용자",
|
||||
"adminRole": "관리자",
|
||||
'searchPlaceholder': '이름, 이메일 또는 역할(admin/user)로 검색...'
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,433 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>黑盒智能开发者平台</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" integrity="sha512-iBBXm8fW90+nuLcSKlbmrPcLa0OT92xO1BIsZ+ywDWZCvqsWgccV3gFoRBv0z+8dLJgyAHIhR35VZc2oM/gI1w==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Arial', sans-serif;
|
||||
}
|
||||
body {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 14px 28px rgba(0,0,0,0.25), 0 10px 10px rgba(0,0,0,0.22);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 80%;
|
||||
max-width: 1200px; /* Set a maximum width */
|
||||
min-height: 900px; /* Use a fixed minimum height */
|
||||
margin: 20px auto; /* Add some margin */
|
||||
}
|
||||
.form-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
transition: all 0.6s ease-in-out;
|
||||
}
|
||||
.sign-in-container {
|
||||
left: 0;
|
||||
width: 50%;
|
||||
z-index: 2;
|
||||
}
|
||||
.sign-up-container {
|
||||
left: 0;
|
||||
width: 50%;
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.container.right-panel-active .sign-in-container {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
.container.right-panel-active .sign-up-container {
|
||||
transform: translateX(100%);
|
||||
opacity: 1;
|
||||
z-index: 5;
|
||||
}
|
||||
form {
|
||||
background-color: #FFFFFF;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 0 50px;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
margin-bottom: 30px;
|
||||
font-size: 3.5em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.2em;
|
||||
margin-bottom: 30px; /* Increase bottom margin */
|
||||
}
|
||||
|
||||
|
||||
input {
|
||||
background-color: #eee;
|
||||
border: none;
|
||||
padding: 12px 15px;
|
||||
margin: 15px 0;
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 50px;
|
||||
border: 1px solid #667eea;
|
||||
background-color: #667eea;
|
||||
color: #FFFFFF;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
padding: 12px 45px;
|
||||
letter-spacing: 3px;
|
||||
text-transform: uppercase;
|
||||
transition: transform 80ms ease-in;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
button:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
button.ghost {
|
||||
background-color: transparent;
|
||||
border-color: #FFFFFF;
|
||||
}
|
||||
.social-container {
|
||||
margin: 20px 0;
|
||||
}
|
||||
.social-container a {
|
||||
border: 1px solid #DDDDDD;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 5px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
.overlay-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: transform 0.6s ease-in-out;
|
||||
z-index: 100;
|
||||
}
|
||||
.container.right-panel-active .overlay-container{
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
.overlay {
|
||||
background: #667eea;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: 0 0;
|
||||
color: #FFFFFF;
|
||||
position: relative;
|
||||
left: -100%;
|
||||
height: 100%;
|
||||
width: 200%;
|
||||
transform: translateX(0);
|
||||
transition: transform 0.6s ease-in-out;
|
||||
}
|
||||
.container.right-panel-active .overlay {
|
||||
transform: translateX(50%);
|
||||
}
|
||||
.overlay-panel {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 0 40px;
|
||||
text-align: center;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 50%;
|
||||
transform: translateX(0);
|
||||
transition: transform 0.6s ease-in-out;
|
||||
}
|
||||
.overlay-left {
|
||||
transform: translateX(-20%);
|
||||
}
|
||||
.container.right-panel-active .overlay-left {
|
||||
transform: translateX(0);
|
||||
}
|
||||
.overlay-right {
|
||||
right: 0;
|
||||
transform: translateX(0);
|
||||
}
|
||||
.container.right-panel-active .overlay-right {
|
||||
transform: translateX(20%);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
width: 90%;
|
||||
min-height: 90vh;
|
||||
}
|
||||
form {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.overlay-panel {
|
||||
padding: 0 20px;
|
||||
}
|
||||
.title {
|
||||
font-size: 2em;
|
||||
}
|
||||
.logo {
|
||||
font-size: 2.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" id="container">
|
||||
<div class="form-container sign-up-container">
|
||||
<form id="signupForm">
|
||||
<h1>Create Account</h1>
|
||||
<!-- <div class="social-container">
|
||||
<a href="#" class="social"><i class="fab fa-google"></i></a>
|
||||
<a href="#" class="social"><i class="fab fa-github"></i></a>
|
||||
</div> -->
|
||||
<span>use your email for registration</span>
|
||||
<input type="text" placeholder="Username" id="signupUsername" required />
|
||||
<input type="password" placeholder="Password" id="signupPassword" required />
|
||||
<div class="password-rules">
|
||||
Password must be at least 8 characters long and include uppercase, lowercase, digit, and special character (!@#$%^&*?:).
|
||||
</div>
|
||||
<input type="email" placeholder="Email" id="signupEmail" required />
|
||||
<input type="tel" placeholder="Phone" id="signupPhone" required />
|
||||
<button type="submit">REGISTER</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="form-container sign-in-container">
|
||||
<form id="signinForm">
|
||||
<h1>Sign in</h1>
|
||||
<!-- <div class="social-container">
|
||||
<a href="#" class="social"><i class="fab fa-google"></i></a>
|
||||
<a href="#" class="social"><i class="fab fa-github"></i></a>
|
||||
</div> -->
|
||||
<span>use your account</span>
|
||||
|
||||
<input type="text" placeholder="Username" id="signinUsername" required />
|
||||
<input type="password" placeholder="Password" id="signinPassword" required />
|
||||
<a href="#">Forgot your password?</a>
|
||||
<button type="submit">Sign In</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="overlay-container">
|
||||
<div class="overlay">
|
||||
<div class="overlay-panel overlay-left">
|
||||
<h1>Welcome Back!</h1>
|
||||
<p>To keep connected with us please login with your personal info</p>
|
||||
<button class="ghost" id="signIn">Sign In</button>
|
||||
</div>
|
||||
<div class="overlay-panel overlay-right">
|
||||
<h1>Hello, Friend!</h1>
|
||||
<p>Start your journey with us</p>
|
||||
<button class="ghost" id="signUp">Sign Up</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const signUpButton = document.getElementById('signUp');
|
||||
const signInButton = document.getElementById('signIn');
|
||||
const container = document.getElementById('container');
|
||||
|
||||
signUpButton.addEventListener('click', () => {
|
||||
container.classList.add("right-panel-active");
|
||||
});
|
||||
|
||||
signInButton.addEventListener('click', () => {
|
||||
container.classList.remove("right-panel-active");
|
||||
});
|
||||
|
||||
const API_URL = 'https://user.obscura.work/user';
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
document.addEventListener('DOMContentLoaded', checkLoginStatus);
|
||||
|
||||
function checkLoginStatus() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
validateToken(token);
|
||||
} else {
|
||||
showLoginForm();
|
||||
}
|
||||
}
|
||||
|
||||
async function validateToken(token) {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/me`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('用户信息:', data);
|
||||
redirectToDashboard();
|
||||
} else {
|
||||
throw new Error('无效的token');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('错误:', error);
|
||||
logout();
|
||||
}
|
||||
}
|
||||
|
||||
function showLoginForm() {
|
||||
container.classList.remove("right-panel-active");
|
||||
}
|
||||
|
||||
function redirectToDashboard(token) {
|
||||
window.location.href = `https://user.obscura.work/platform.html`;
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
try {
|
||||
await fetch(`${API_URL}/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('登出时发生错误:', error);
|
||||
}
|
||||
}
|
||||
localStorage.removeItem('access_token');
|
||||
showLoginForm();
|
||||
}
|
||||
|
||||
// 注册
|
||||
document.getElementById('signupForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('signupUsername').value;
|
||||
const password = document.getElementById('signupPassword').value;
|
||||
const email = document.getElementById('signupEmail').value;
|
||||
const phoneNumber = document.getElementById('signupPhone').value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: username,
|
||||
email: email,
|
||||
phone_number: phoneNumber,
|
||||
password: password
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
alert(data.message);
|
||||
showLoginForm();
|
||||
} else {
|
||||
let errorMessage = '注册失败: ';
|
||||
if (typeof data.detail === 'object') {
|
||||
for (let key in data.detail) {
|
||||
errorMessage += `${key}: ${data.detail[key].join(', ')}\n`;
|
||||
}
|
||||
} else {
|
||||
errorMessage += data.detail || '发生未知错误';
|
||||
}
|
||||
alert(errorMessage);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('错误:', error);
|
||||
alert('发生错误,请重试。');
|
||||
}
|
||||
});
|
||||
|
||||
// 登录
|
||||
document.getElementById('signinForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const username = document.getElementById('signinUsername').value;
|
||||
const password = document.getElementById('signinPassword').value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`,
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (response.ok) {
|
||||
localStorage.setItem('access_token', data.access_token);
|
||||
alert('登录成功');
|
||||
redirectToDashboard(data.access_token);
|
||||
} else {
|
||||
alert(data.detail || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('错误:', error);
|
||||
alert('发生错误,请重试。');
|
||||
}
|
||||
});
|
||||
|
||||
// 处理需要认证的API请求
|
||||
async function makeAuthenticatedRequest(url, options = {}) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
throw new Error('未找到token');
|
||||
}
|
||||
|
||||
const headers = new Headers(options.headers || {});
|
||||
headers.append('Authorization', `Bearer ${token}`);
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401) {
|
||||
logout();
|
||||
throw new Error('未授权');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,870 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
if (themeToggle) {
|
||||
themeToggle.addEventListener('click', () => {
|
||||
document.body.classList.toggle('dark-mode');
|
||||
themeToggle.innerHTML = document.body.classList.contains('dark-mode')
|
||||
? '<i class="fas fa-sun"></i>'
|
||||
: '<i class="fas fa-moon"></i>';
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化用户管理相关的功能
|
||||
initUserManagement();
|
||||
|
||||
// 添加用户表单提交
|
||||
document.getElementById('addUserForm').addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
const userData = {
|
||||
username: document.getElementById('name').value.trim(),
|
||||
email: document.getElementById('email').value.trim(),
|
||||
password: document.getElementById('password').value,
|
||||
phone_number: document.getElementById('phoneNumber').value.trim() || null,
|
||||
role: document.getElementById('role').value,
|
||||
is_active: false,
|
||||
email_verified: false,
|
||||
phone_verified: false
|
||||
};
|
||||
|
||||
try {
|
||||
// 前端验证
|
||||
validateUserData(userData);
|
||||
|
||||
const token = localStorage.getItem('access_token');
|
||||
|
||||
const response = await fetch(`${API_URL}/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(userData)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
console.log('服务器返回的完整错误信息:', JSON.stringify(errorData, null, 2));
|
||||
|
||||
if (Array.isArray(errorData.detail)) {
|
||||
const firstError = errorData.detail[0];
|
||||
throw new Error(firstError?.msg || lang[selectedLang].addUserFailed);
|
||||
}
|
||||
|
||||
throw new Error(errorData.detail || lang[selectedLang].addUserFailed);
|
||||
}
|
||||
|
||||
fetchUsers();
|
||||
closeAddUserModal();
|
||||
this.reset(); // 重置表单
|
||||
showToast(lang[selectedLang].addUserSuccess, 'success');
|
||||
} catch (error) {
|
||||
console.error(lang[selectedLang].addUserError, error);
|
||||
showToast(error.message || lang[selectedLang].addUserFailed, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 初始化用户管理相关的功能
|
||||
initUserManagement();
|
||||
});
|
||||
|
||||
function initUserManagement() {
|
||||
// 初始化用户管理相关的功能
|
||||
checkLoginStatus();
|
||||
|
||||
showFeature();
|
||||
}
|
||||
|
||||
const API_URL = 'https://user.obscura.work/user';
|
||||
let currentUser = null;
|
||||
|
||||
// 获取用户信息的函数
|
||||
async function fetchUserInfo() {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
throw new Error('No access token found');
|
||||
}
|
||||
|
||||
const response = await makeAuthenticatedRequest(`${API_URL}/me`);
|
||||
|
||||
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);
|
||||
logout();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户显示的函数
|
||||
function updateUserDisplay() {
|
||||
const userName = document.getElementById('userName');
|
||||
const userAvatar = document.getElementById('userAvatar');
|
||||
const dashboardContent = document.getElementById('dashboardContent');
|
||||
const usersContent = document.getElementById('usersContent');
|
||||
|
||||
if (currentUser) {
|
||||
userName.textContent = currentUser.username;
|
||||
userAvatar.textContent = currentUser.username.charAt(0).toUpperCase();
|
||||
if (dashboardContent) dashboardContent.style.display = 'block';
|
||||
if (usersContent) usersContent.style.display = 'none';
|
||||
if (userCenterContent) userCenterContent.style.display = 'none';
|
||||
|
||||
} else {
|
||||
userName.textContent = 'Guest';
|
||||
userAvatar.textContent = 'G';
|
||||
if (dashboardContent) dashboardContent.style.display = 'none';
|
||||
if (usersContent) usersContent.style.display = 'none';
|
||||
if (userCenterContent) userCenterContent.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 登出函数
|
||||
async function logout() {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
try {
|
||||
await fetch(`${API_URL}/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(lang[selectedLang].logoutError, error);
|
||||
}
|
||||
}
|
||||
localStorage.removeItem('access_token');
|
||||
currentUser = null;
|
||||
updateUserDisplay();
|
||||
window.location.href = 'login.html'; // 重定向到登录页面
|
||||
}
|
||||
|
||||
// 显示仪表板
|
||||
async function showDashboard() {
|
||||
try {
|
||||
const userInfo = await fetchUserInfo();
|
||||
if (!userInfo) {
|
||||
alert(lang[selectedLang].pleaseLoginToViewDashboard);
|
||||
window.location.href = 'login.html'; // 重定向到登录页面
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查邮箱验证状态
|
||||
const emailVerified = userInfo.email_verified === 'True' || userInfo.email_verified === true;
|
||||
console.log('Email Verified:', emailVerified); // 打印邮箱验证状态
|
||||
if (!emailVerified) {
|
||||
alert(lang[selectedLang].pleaseVerifyEmail);
|
||||
showUserCenterSection('profile'); // 跳转到个人资料页面
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果用户已登录且邮箱已验证,继续显示仪表盘
|
||||
const pageTitle = document.getElementById('pageTitle');
|
||||
const dashboardContent = document.getElementById('dashboardContent');
|
||||
const usersContent = document.getElementById('usersContent');
|
||||
const chatContent = document.getElementById('chatContent');
|
||||
const featureContent = document.getElementById('featureContent');
|
||||
const userCenterContent = document.getElementById('userCenterContent');
|
||||
const showApiDocs = document.getElementById('showApiDocs');
|
||||
const apiDocsContent = document.getElementById('apiDocsContent');
|
||||
;
|
||||
if (!userInfo) {
|
||||
console.error('No user info found');
|
||||
return;
|
||||
}
|
||||
|
||||
if (apiDocsContent) apiDocsContent.style.display = 'none'
|
||||
if (showApiDocs) showApiDocs.style.display = 'none';
|
||||
if (userCenterContent) userCenterContent.style.display = 'none';
|
||||
if (chatContent) chatContent.style.display = 'none';
|
||||
if (featureContent) featureContent.style.display = 'none';
|
||||
if (usersContent) usersContent.style.display = 'none';
|
||||
if (pageTitle) pageTitle.textContent = lang[selectedLang].dashboard;
|
||||
if (dashboardContent) {
|
||||
dashboardContent.style.display = 'block';
|
||||
dashboardContent.innerHTML = `
|
||||
<h2>Hello, ${userInfo.username}!</h2>
|
||||
<section class="dashboard-cards">
|
||||
<div class="card">
|
||||
<i class="fas fa-users card-icon"></i>
|
||||
<h3 class="card-title" data-i18n="totalUsers">Total Users</h3>
|
||||
<p class="card-value" id="totalUsers">Loading...</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<i class="fas fa-user-plus card-icon"></i>
|
||||
<h3 class="card-title" data-i18n="newUsersToday">New Users Today</h3>
|
||||
<p class="card-value" id="newUsers">Loading...</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<i class="fas fa-user-check card-icon"></i>
|
||||
<h3 class="card-title" data-i18n="activeUsers">Active Users</h3>
|
||||
<p class="card-value" id="activeUsers">Loading...</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="recent-activities">
|
||||
<h3 data-i18n="recentNewUsers">Recent New Users</h3>
|
||||
<ul class="activity-list" id="activityList">
|
||||
<li data-i18n="loading">Loading recent users...</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div class="chart-container">
|
||||
<canvas id="userChart"></canvas>
|
||||
</div>
|
||||
`;
|
||||
fetchDashboardData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in showDashboard:', error);
|
||||
alert(lang[selectedLang].errorGettingUserInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取仪表板数据
|
||||
async function fetchDashboardData() {
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`${API_URL}/dashboard`);
|
||||
if (!response.ok) {
|
||||
throw new Error(lang[selectedLang].fetchDashboardDataFailed);
|
||||
}
|
||||
const data = await response.json();
|
||||
updateDashboardUI(data);
|
||||
fetchUserStats();
|
||||
} catch (error) {
|
||||
console.error('Error fetching dashboard data:', error);
|
||||
alert(lang[selectedLang].fetchDashboardDataFailed);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUserStats() {
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`${API_URL}/user-stats`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch user stats');
|
||||
}
|
||||
const data = await response.json();
|
||||
createUserChart(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching user stats:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新仪表板UI
|
||||
function updateDashboardUI(data) {
|
||||
try {
|
||||
// 更新用户统计数据
|
||||
const totalUsersEl = document.getElementById('totalUsers');
|
||||
const newUsersEl = document.getElementById('newUsers');
|
||||
const activeUsersEl = document.getElementById('activeUsers');
|
||||
|
||||
if (totalUsersEl) totalUsersEl.textContent = data.total_users;
|
||||
if (newUsersEl) newUsersEl.textContent = data.new_users_today;
|
||||
if (activeUsersEl) activeUsersEl.textContent = data.active_users;
|
||||
|
||||
// 更新最近用户列表
|
||||
const activityList = document.getElementById('activityList');
|
||||
if (activityList) {
|
||||
activityList.innerHTML = '';
|
||||
if (data.recent_users && Array.isArray(data.recent_users) && data.recent_users.length > 0) {
|
||||
data.recent_users.forEach(user => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'activity-item';
|
||||
li.innerHTML = `
|
||||
<i class="fas fa-user-plus activity-icon"></i>
|
||||
<div class="activity-details">
|
||||
<p class="activity-description">${lang[selectedLang].newUser}: ${user.username}</p>
|
||||
<p class="activity-time">${formatActivityTime(user.created_at)}</p>
|
||||
</div>
|
||||
`;
|
||||
activityList.appendChild(li);
|
||||
});
|
||||
} else {
|
||||
console.warn(lang[selectedLang].noRecentUsersData);
|
||||
activityList.innerHTML = `<li>${lang[selectedLang].noRecentUserActivities}</li>`;
|
||||
}
|
||||
} else {
|
||||
console.error(lang[selectedLang].activityListNotFound);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating dashboard UI:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化活动时间
|
||||
function formatActivityTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffTime = Math.abs(now - date);
|
||||
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
const diffHours = Math.floor(diffTime / (1000 * 60 * 60));
|
||||
const diffMinutes = Math.floor(diffTime / (1000 * 60));
|
||||
|
||||
if (diffDays > 0) {
|
||||
return `${diffDays} ${lang[selectedLang].daysAgo}`;
|
||||
} else if (diffHours > 0) {
|
||||
return `${diffHours} ${lang[selectedLang].hoursAgo}`;
|
||||
} else if (diffMinutes > 0) {
|
||||
return `${diffMinutes} ${lang[selectedLang].minutesAgo}`;
|
||||
} else {
|
||||
return lang[selectedLang].justNow;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建用户统计图表
|
||||
function createUserChart(userStats) {
|
||||
const ctx = document.getElementById('userChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: userStats.dates,
|
||||
datasets: [{
|
||||
label: lang[selectedLang].newUsersToday,
|
||||
data: userStats.daily_new_users,
|
||||
backgroundColor: 'rgba(102, 126, 234, 0.2)',
|
||||
borderColor: 'rgba(102, 126, 234, 1)',
|
||||
borderWidth: 2,
|
||||
pointBackgroundColor: 'rgba(102, 126, 234, 1)',
|
||||
pointBorderColor: '#fff',
|
||||
pointHoverBackgroundColor: '#fff',
|
||||
pointHoverBorderColor: 'rgba(102, 126, 234, 1)'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: lang[selectedLang].numberOfNewUsers
|
||||
}
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: lang[selectedLang].date
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// 显示用户列表
|
||||
let currentPage = 1;
|
||||
const usersPerPage = 10; // 每页显示的用户数量
|
||||
let allUsers = []; // 存储所有用户数据
|
||||
|
||||
// 在文件开头添加变量
|
||||
let searchUserTimeout = null;
|
||||
|
||||
// 修改 showUsers 函数,添加搜索框初始化
|
||||
async function showUsers() {
|
||||
try {
|
||||
const userInfo = await fetchUserInfo();
|
||||
if (!userInfo) {
|
||||
alert(lang[selectedLang].pleaseLoginToViewUserList);
|
||||
window.location.href = 'login.html';
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查邮箱验证状态
|
||||
const emailVerified = userInfo.email_verified === 'True' || userInfo.email_verified === true;
|
||||
if (!emailVerified) {
|
||||
alert(lang[selectedLang].pleaseCompleteEmailVerification);
|
||||
showUserCenterSection('profile');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新页面内容
|
||||
const pageTitle = document.getElementById('pageTitle');
|
||||
const dashboardContent = document.getElementById('dashboardContent');
|
||||
const usersContent = document.getElementById('usersContent');
|
||||
const chatContent = document.getElementById('chatContent');
|
||||
const featureContent = document.getElementById('featureContent');
|
||||
const userCenterContent = document.getElementById('userCenterContent');
|
||||
const apiDocsContent = document.getElementById('apiDocsContent');
|
||||
|
||||
if (apiDocsContent) apiDocsContent.style.display = 'none';
|
||||
if (dashboardContent) dashboardContent.style.display = 'none';
|
||||
if (featureContent) featureContent.style.display = 'none';
|
||||
if (usersContent) usersContent.style.display = 'block';
|
||||
if (chatContent) chatContent.style.display = 'none';
|
||||
if (userCenterContent) userCenterContent.style.display = 'none';
|
||||
if (pageTitle) pageTitle.innerText = lang[selectedLang].userManagement;
|
||||
|
||||
// 初始化搜索功能
|
||||
initializeUserSearch();
|
||||
|
||||
fetchUsers();
|
||||
} catch (error) {
|
||||
console.error('Error in showUsers:', error);
|
||||
alert(lang[selectedLang].errorGettingUserInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// 修改搜索初始化函数
|
||||
function initializeUserSearch() {
|
||||
const searchInput = document.getElementById('userSearch');
|
||||
const searchIcon = document.querySelector('.user-search-icon');
|
||||
|
||||
if (searchInput) {
|
||||
// 添加占位符提示
|
||||
searchInput.placeholder = '搜索用户名、邮箱或角色(admin/user)...';
|
||||
|
||||
// 输入事件处理(实时搜索)
|
||||
searchInput.addEventListener('input', function(e) {
|
||||
if (searchUserTimeout) {
|
||||
clearTimeout(searchUserTimeout);
|
||||
}
|
||||
searchUserTimeout = setTimeout(() => {
|
||||
searchUsers(e.target.value);
|
||||
}, 300);
|
||||
});
|
||||
|
||||
// 回车键事件处理
|
||||
searchInput.addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
searchUsers(this.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 点击搜索图标事件处理
|
||||
if (searchIcon) {
|
||||
searchIcon.addEventListener('click', function() {
|
||||
searchUsers(searchInput.value);
|
||||
});
|
||||
searchIcon.style.cursor = 'pointer';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改搜索用户函数
|
||||
async function searchUsers(query) {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
const response = await fetch(`${API_URL}/api/users-search?query=${encodeURIComponent(query)}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('搜索失败');
|
||||
}
|
||||
|
||||
const searchResults = await response.json();
|
||||
allUsers = searchResults; // 更新全局用户列表
|
||||
currentPage = 1; // 重置到第一页
|
||||
updatePagination();
|
||||
displayUsers();
|
||||
|
||||
// 如果没有搜索结果,显示提示
|
||||
if (searchResults.length === 0) {
|
||||
showToast('未找到匹配的用户', 'info');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('搜索用户时出错:', error);
|
||||
showToast(error.message || lang[selectedLang].searchFailed, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
async function fetchUsers() {
|
||||
try {
|
||||
const response = await makeAuthenticatedRequest(`${API_URL}/api/users`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
allUsers = await response.json();
|
||||
updatePagination();
|
||||
displayUsers();
|
||||
return allUsers;
|
||||
} catch (error) {
|
||||
console.error('Error fetching users:', error);
|
||||
alert('Failed to fetch users. Please try again.');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 更新分页信息
|
||||
function updatePagination() {
|
||||
const totalPages = Math.ceil(allUsers.length / usersPerPage);
|
||||
const paginationDiv = document.querySelector('.pagination');
|
||||
let paginationHTML = `
|
||||
<a href="#" class="page-nav" onclick="changePage(-1)"><</a>
|
||||
`;
|
||||
|
||||
const showPages = (start, end) => {
|
||||
for (let i = start; i <= end; i++) {
|
||||
paginationHTML += `
|
||||
<a href="#" class="page-number ${i === currentPage ? 'active' : ''}" onclick="goToPage(${i})">${i}</a>
|
||||
`;
|
||||
}
|
||||
};
|
||||
|
||||
if (totalPages <= 7) {
|
||||
showPages(1, totalPages);
|
||||
} else {
|
||||
if (currentPage <= 3) {
|
||||
showPages(1, 5);
|
||||
paginationHTML += `<span class="page-ellipsis">...</span>`;
|
||||
paginationHTML += `<a href="#" class="page-number" onclick="goToPage(${totalPages})">${totalPages}</a>`;
|
||||
} else if (currentPage >= totalPages - 2) {
|
||||
paginationHTML += `<a href="#" class="page-number" onclick="goToPage(1)">1</a>`;
|
||||
paginationHTML += `<span class="page-ellipsis">...</span>`;
|
||||
showPages(totalPages - 4, totalPages);
|
||||
} else {
|
||||
paginationHTML += `<a href="#" class="page-number" onclick="goToPage(1)">1</a>`;
|
||||
paginationHTML += `<span class="page-ellipsis">...</span>`;
|
||||
showPages(currentPage - 1, currentPage + 1);
|
||||
paginationHTML += `<span class="page-ellipsis">...</span>`;
|
||||
paginationHTML += `<a href="#" class="page-number" onclick="goToPage(${totalPages})">${totalPages}</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
paginationHTML += `
|
||||
<a href="#" class="page-nav" onclick="changePage(1)">></a>
|
||||
`;
|
||||
|
||||
paginationDiv.innerHTML = paginationHTML;
|
||||
}
|
||||
|
||||
// 切换到指定页面
|
||||
function goToPage(page) {
|
||||
currentPage = page;
|
||||
displayUsers();
|
||||
updatePagination();
|
||||
}
|
||||
|
||||
// 修改changePage函数
|
||||
function changePage(direction) {
|
||||
const totalPages = Math.ceil(allUsers.length / usersPerPage);
|
||||
currentPage += direction;
|
||||
if (currentPage < 1) currentPage = 1;
|
||||
if (currentPage > totalPages) currentPage = totalPages;
|
||||
displayUsers();
|
||||
updatePagination();
|
||||
}
|
||||
|
||||
|
||||
// 修改显示用户函数
|
||||
function displayUsers() {
|
||||
const start = (currentPage - 1) * usersPerPage;
|
||||
const end = start + usersPerPage;
|
||||
const pageUsers = allUsers.slice(start, end);
|
||||
|
||||
const userTableBody = document.getElementById('userTableBody');
|
||||
userTableBody.innerHTML = '';
|
||||
|
||||
if (pageUsers.length === 0) {
|
||||
// 果没有用户数据,显示一个提示行
|
||||
const emptyRow = document.createElement('tr');
|
||||
emptyRow.innerHTML = `
|
||||
<td colspan="5" class="text-center">${lang[selectedLang].noUsersFound}</td>
|
||||
`;
|
||||
userTableBody.appendChild(emptyRow);
|
||||
return;
|
||||
}
|
||||
|
||||
pageUsers.forEach((user, index) => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${(currentPage - 1) * usersPerPage + index + 1}</td>
|
||||
<td>${user.username}</td>
|
||||
<td>${user.email}</td>
|
||||
<td>${user.role}</td>
|
||||
<td>
|
||||
<button onclick="editUser('${user.id}')"><i class="fas fa-edit"></i></button>
|
||||
<button onclick="deleteUser('${user.id}')"><i class="fas fa-trash"></i></button>
|
||||
</td>
|
||||
`;
|
||||
userTableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// 修改编辑用户函数
|
||||
async function editUser(id) {
|
||||
try {
|
||||
// 获取当前用户信息
|
||||
const userResponse = await makeAuthenticatedRequest(`${API_URL}/api/users/${id}`);
|
||||
if (!userResponse.ok) {
|
||||
throw new Error(lang[selectedLang].getUserInfoFailed);
|
||||
}
|
||||
const userData = await userResponse.json();
|
||||
|
||||
// 创建编辑表单
|
||||
const formContent = `
|
||||
<div class="edit-user-form">
|
||||
<div class="form-group">
|
||||
<label for="edit-username">${lang[selectedLang].username}</label>
|
||||
<input type="text" id="edit-username" value="${userData.username}" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-role">${lang[selectedLang].role}</label>
|
||||
<select id="edit-role">
|
||||
<option value="admin" ${userData.role === 'admin' ? 'selected' : ''}>Admin</option>
|
||||
<option value="user" ${userData.role === 'user' ? 'selected' : ''}>User</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 使用自定义对话框显示表单
|
||||
const result = await showCustomDialog(
|
||||
lang[selectedLang].editUser,
|
||||
formContent,
|
||||
lang[selectedLang].save,
|
||||
lang[selectedLang].cancel
|
||||
);
|
||||
|
||||
if (result && result.confirmed) {
|
||||
const { username, role } = result.formData;
|
||||
|
||||
if (!username) {
|
||||
throw new Error(lang[selectedLang].usernameRequired);
|
||||
}
|
||||
|
||||
const response = await makeAuthenticatedRequest(`${API_URL}/edit/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
role
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || lang[selectedLang].updateUserFailed);
|
||||
}
|
||||
|
||||
await fetchUsers(); // 重新获取用户列表
|
||||
showToast(lang[selectedLang].userInfoUpdateSuccess, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error editing user:', error);
|
||||
showToast(error.message || lang[selectedLang].updateUserFailed, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 修改自定义对话框函数
|
||||
function showCustomDialog(title, content, confirmText, cancelText) {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = document.createElement('div');
|
||||
dialog.className = 'custom-dialog';
|
||||
dialog.innerHTML = `
|
||||
<div class="dialog-content">
|
||||
<h2>${title}</h2>
|
||||
<div>${content}</div>
|
||||
<div class="dialog-buttons">
|
||||
<button class="btn-confirm">${confirmText}</button>
|
||||
<button class="btn-cancel">${cancelText}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(dialog);
|
||||
|
||||
const confirmBtn = dialog.querySelector('.btn-confirm');
|
||||
const cancelBtn = dialog.querySelector('.btn-cancel');
|
||||
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
const username = dialog.querySelector('#edit-username')?.value;
|
||||
const role = dialog.querySelector('#edit-role')?.value;
|
||||
document.body.removeChild(dialog);
|
||||
resolve({
|
||||
confirmed: true,
|
||||
formData: { username, role }
|
||||
});
|
||||
});
|
||||
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
document.body.removeChild(dialog);
|
||||
resolve({ confirmed: false });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 修改显示提示消息函数
|
||||
function showToast(message, type = 'info') {
|
||||
try {
|
||||
// 移除已存在的提示框
|
||||
const existingToast = document.querySelector('.toast-container');
|
||||
if (existingToast && existingToast.parentNode) {
|
||||
existingToast.parentNode.removeChild(existingToast);
|
||||
}
|
||||
|
||||
// 创建提示框容器
|
||||
const toastContainer = document.createElement('div');
|
||||
toastContainer.className = 'toast-container';
|
||||
|
||||
// 创建提示框
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
|
||||
// 添加图标
|
||||
const icon = document.createElement('i');
|
||||
switch (type) {
|
||||
case 'success':
|
||||
icon.className = 'fas fa-check-circle';
|
||||
break;
|
||||
case 'error':
|
||||
icon.className = 'fas fa-exclamation-circle';
|
||||
break;
|
||||
case 'info':
|
||||
icon.className = 'fas fa-info-circle';
|
||||
break;
|
||||
case 'warning':
|
||||
icon.className = 'fas fa-exclamation-triangle';
|
||||
break;
|
||||
}
|
||||
|
||||
// 创建消息文本元素
|
||||
const messageText = document.createElement('span');
|
||||
messageText.textContent = message;
|
||||
|
||||
// 组装提示框
|
||||
toast.appendChild(icon);
|
||||
toast.appendChild(messageText);
|
||||
toastContainer.appendChild(toast);
|
||||
document.body.appendChild(toastContainer);
|
||||
|
||||
// 添加显示动画
|
||||
setTimeout(() => {
|
||||
toast.classList.add('show');
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
setTimeout(() => {
|
||||
if (toastContainer && toastContainer.parentNode) {
|
||||
toastContainer.parentNode.removeChild(toastContainer);
|
||||
}
|
||||
}, 300);
|
||||
}, 3000);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('Error showing toast:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 修改删除用户函数
|
||||
async function deleteUser(id) {
|
||||
try {
|
||||
const confirmed = await showCustomDialog(
|
||||
lang[selectedLang].confirmDelete,
|
||||
`<p>${lang[selectedLang].confirmDeleteUserMessage}</p>`,
|
||||
lang[selectedLang].delete,
|
||||
lang[selectedLang].cancel
|
||||
);
|
||||
|
||||
if (confirmed) {
|
||||
const response = await makeAuthenticatedRequest(`${API_URL}/api/users/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.detail || lang[selectedLang].deleteUserFailed);
|
||||
}
|
||||
|
||||
await fetchUsers(); // 重新获取用户列表
|
||||
showToast(lang[selectedLang].userDeleteSuccess, 'success');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting user:', error);
|
||||
showToast(error.message || lang[selectedLang].deleteUserFailed, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 打开添加用户模态框
|
||||
function openAddUserModal() {
|
||||
const modal = document.getElementById('addUserModal');
|
||||
const form = document.getElementById('addUserForm');
|
||||
form.reset(); // 确保表单是空的
|
||||
modal.style.display = 'block';
|
||||
}
|
||||
|
||||
// 关闭添加用户模态框
|
||||
function closeAddUserModal() {
|
||||
const modal = document.getElementById('addUserModal');
|
||||
const form = document.getElementById('addUserForm');
|
||||
modal.style.display = 'none';
|
||||
form.reset(); // 重置表单
|
||||
}
|
||||
|
||||
|
||||
async function makeAuthenticatedRequest(url, options = {}) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (!token) {
|
||||
throw new Error('未找到token');
|
||||
}
|
||||
|
||||
const headers = new Headers(options.headers || {});
|
||||
headers.append('Authorization', `Bearer ${token}`);
|
||||
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401) {
|
||||
logout();
|
||||
throw new Error('未授权');
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// 导出用户数据为CSV
|
||||
function exportUserData() {
|
||||
if (!allUsers || allUsers.length === 0) {
|
||||
alert(lang[selectedLang].noUserDataToExport);
|
||||
return;
|
||||
}
|
||||
|
||||
const csvContent = "data:text/csv;charset=utf-8,"
|
||||
+ "用户名,邮箱,角色\n"
|
||||
+ allUsers.map(user => `${user.username},${user.email},${user.role}`).join("\n");
|
||||
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", encodedUri);
|
||||
link.setAttribute("download", "user_data.csv");
|
||||
document.body.appendChild(link);
|
||||
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
// 添加数据验证函数
|
||||
function validateUserData(userData) {
|
||||
// 用户名验证:只验证必填
|
||||
if (!userData.username || userData.username.trim() === '') {
|
||||
throw new Error('用户名不能为空');
|
||||
}
|
||||
|
||||
// 邮箱验证:必填且唯一,格式正确
|
||||
if (!userData.email || !userData.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
|
||||
throw new Error('请输入有效的邮箱地址');
|
||||
}
|
||||
|
||||
// 密码验证
|
||||
if (!userData.password || userData.password.length < 6) {
|
||||
throw new Error('密码至少需要6个字符');
|
||||
}
|
||||
|
||||
// 手机号验证:如果提供则必须格式正确
|
||||
if (userData.phone_number && userData.phone_number !== '') {
|
||||
if (!userData.phone_number.match(/^\d{11}$/)) {
|
||||
throw new Error('请输入有效的手机号码');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
const API_ENDPOINT = 'https://dev.obscura.work/v1';
|
||||
|
||||
// 确保 selectedLang 在 module.js 中可用
|
||||
if (typeof selectedLang === 'undefined') {
|
||||
selectedLang = 'zh'; // 如果未定义,设置默认值
|
||||
}
|
||||
|
||||
async function showFeature() {
|
||||
try {
|
||||
const userInfo = await fetchUserInfo();
|
||||
if (!userInfo) {
|
||||
alert(lang[selectedLang].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(lang[selectedLang].pleaseVerifyEmail);
|
||||
showUserCenterSection('profile');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果用户已登录且邮箱已验证,继续显示功能
|
||||
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 apiKeyInput = document.getElementById('apiKeyInput');
|
||||
const showApiDocs = document.getElementById('showApiDocs');
|
||||
const apiDocsContent = document.getElementById('apiDocsContent');
|
||||
|
||||
if (apiDocsContent) apiDocsContent.style.display = 'none';
|
||||
if (featureContent) featureContent.style.display = 'flex';
|
||||
if (chatContent) chatContent.style.display = 'none';
|
||||
if (dashboardContent) dashboardContent.style.display = 'none';
|
||||
if (usersContent) usersContent.style.display = 'none';
|
||||
if (userCenterContent) userCenterContent.style.display = 'none';
|
||||
if (showApiDocs) showApiDocs.style.display = 'none';
|
||||
if (pageTitle) pageTitle.innerText = lang[selectedLang].featureDemo;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in showFeature:', error);
|
||||
alert(lang[selectedLang].errorGettingUserInfo);
|
||||
}
|
||||
}
|
||||
|
||||
function displayTokenUsage(data) {
|
||||
console.log(lang[selectedLang].tokenUsage, data);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 首先声明所有需要的变量
|
||||
const aiInput = document.getElementById('aiInput');
|
||||
const fileInput = document.getElementById('fileInput');
|
||||
const fileInputButton = document.getElementById('fileInputButton');
|
||||
const takePictureButton = document.getElementById('takePictureButton');
|
||||
const recordVideoButton = document.getElementById('recordVideoButton');
|
||||
const clearButton = document.getElementById('clearButton');
|
||||
const stopRecordingButton = document.getElementById('stopRecordingButton');
|
||||
const previewArea = document.getElementById('previewArea');
|
||||
const resultArea = document.getElementById('resultArea');
|
||||
const loadingIndicator = document.getElementById('loadingIndicator');
|
||||
const poseButton = document.getElementById('poseButton');
|
||||
const fallButton = document.getElementById('fallButton');
|
||||
const cpmButton = document.getElementById('cpmButton');
|
||||
const objectButton = document.getElementById('objectButton');
|
||||
const faceButton = document.getElementById('faceButton');
|
||||
const compareButton = document.getElementById('compareButton');
|
||||
const mediapipeButton = document.getElementById('mediapipeButton');
|
||||
const qwenvlButton = document.getElementById('qwenvlButton');
|
||||
const cpmanalyzeButton = document.getElementById('cpmanalyzeButton');
|
||||
const qwenvlanalyzeButton = document.getElementById('qwenvlanalyzeButton');
|
||||
let mediaRecorder;
|
||||
let recordedChunks = [];
|
||||
// 定义任务类型
|
||||
const TASK_TYPES = {
|
||||
POSE: 'pose',
|
||||
CPM: 'cpm',
|
||||
QWENVL: 'qwenvl',
|
||||
YOLO: 'yolo',
|
||||
FALL: 'fall',
|
||||
FACE: 'face',
|
||||
MEDIAPIPE: 'mediapipe',
|
||||
COMPARE: 'compare',
|
||||
CPM_ANALYZE: 'cpm_analyze',
|
||||
QWENVL_ANALYZE: 'qwenvl_analyze'
|
||||
};
|
||||
|
||||
// 然后添加事件监听器
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
previewArea.innerHTML = '';
|
||||
if (file.type.startsWith('image/')) {
|
||||
const img = document.createElement('img');
|
||||
img.src = e.target.result;
|
||||
previewArea.appendChild(img);
|
||||
} else if (file.type.startsWith('video/')) {
|
||||
const video = document.createElement('video');
|
||||
video.src = e.target.result;
|
||||
video.controls = true;
|
||||
previewArea.appendChild(video);
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (fileInputButton) {
|
||||
fileInputButton.addEventListener('click', function() {
|
||||
fileInput.click();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function uploadFile(taskType) {
|
||||
const file = fileInput.files[0];
|
||||
console.log("Selected file:", file);
|
||||
const apiKey = apiKeyInput.value.trim();
|
||||
console.log("API Key:", apiKey);
|
||||
|
||||
if (!file) {
|
||||
alert(lang[selectedLang].pleaseSelectFile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!apiKey) {
|
||||
alert(lang[selectedLang].pleaseEnterApiKey);
|
||||
return;
|
||||
}
|
||||
|
||||
loadingIndicator.style.display = 'block';
|
||||
disableAllButtons();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
console.log(`Sending request to: ${API_ENDPOINT}/${taskType}`);
|
||||
fetch(`${API_ENDPOINT}/${taskType}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Upload response:', data);
|
||||
if (data.task_id) {
|
||||
pollResult(data.task_id, apiKey, taskType);
|
||||
displayTokenUsage(data);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert(lang[selectedLang].errorUploadingFile);
|
||||
loadingIndicator.style.display = 'none';
|
||||
enableAllButtons();
|
||||
});
|
||||
}
|
||||
|
||||
// 新的轮询函数
|
||||
function pollResult(taskId, apiKey, taskType) {
|
||||
const pollInterval = setInterval(() => {
|
||||
fetch(`${API_ENDPOINT}/result/${taskId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Poll response:', data);
|
||||
if (data.status === 'completed') {
|
||||
clearInterval(pollInterval);
|
||||
addResultCard(getAnalysisTitle(taskType), data);
|
||||
if (taskType !== 'cpm' && taskType !== 'qwenvl' && taskType !== 'qwenvl_analyze' && taskType !== 'cpm_analyze') {
|
||||
fetchAnnotatedFile(API_ENDPOINT, taskId, apiKey);
|
||||
}
|
||||
loadingIndicator.style.display = 'none';
|
||||
enableAllButtons();
|
||||
} else if (data.status === 'failed') {
|
||||
clearInterval(pollInterval);
|
||||
alert(lang[selectedLang].processingFailed);
|
||||
loadingIndicator.style.display = 'none';
|
||||
enableAllButtons();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
clearInterval(pollInterval);
|
||||
alert(lang[selectedLang].errorGettingResults);
|
||||
loadingIndicator.style.display = 'none';
|
||||
enableAllButtons();
|
||||
});
|
||||
}, 2000); // 每2秒轮询一次
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 辅助函数
|
||||
function getAnalysisTitle(analysisType) {
|
||||
switch(analysisType) {
|
||||
case 'pose': return '姿态分析';
|
||||
case 'fall': return '跌倒检测';
|
||||
case 'cpm': return 'MiniCPM内容分析';
|
||||
case 'object': return '目标检测';
|
||||
case 'face': return '人脸检测';
|
||||
case 'compare': return '面部特征提取';
|
||||
case 'mediapipe': return 'mediapipe面部识别';
|
||||
case 'qwenvl': return 'Qwen内容分析';
|
||||
case 'qwenvl_analyze': return 'qwenvl_OCR分析';
|
||||
case 'cpm_analyze': return 'cpm_OCR分析';
|
||||
default: return '分析结果';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function fetchAnnotatedFile(apiEndpoint, taskId, apiKey) {
|
||||
fetch(`${apiEndpoint}/annotated/${taskId}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${apiKey}`
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.blob();
|
||||
}
|
||||
if (response.status === 404) {
|
||||
throw new Error(lang[selectedLang].resultImageNotFound);
|
||||
}
|
||||
throw new Error('Network response was not ok.');
|
||||
})
|
||||
.then(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
updatePreviewWithAnnotated(url, `result_${taskId}.png`);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
if (error.message === lang[selectedLang].resultImageNotFound) {
|
||||
alert(lang[selectedLang].resultImageNotFound);
|
||||
} else {
|
||||
alert(lang[selectedLang].errorGettingResultImage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addResultCard(title, data) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'result-card';
|
||||
|
||||
card.innerHTML = `
|
||||
<h3>${title}</h3>
|
||||
<pre>${JSON.stringify(data, null, 2)}</pre>
|
||||
`;
|
||||
|
||||
resultArea.innerHTML = `<h3>${lang[selectedLang].detectionResult}</h3>`; // 保留标题
|
||||
resultArea.appendChild(card);
|
||||
}
|
||||
|
||||
function updatePreviewWithAnnotated(url, filename) {
|
||||
previewArea.innerHTML = '';
|
||||
|
||||
const isImage = filename.match(/\.(jpg|jpeg|png|gif)$/i);
|
||||
|
||||
if (isImage) {
|
||||
const img = document.createElement('img');
|
||||
img.src = url;
|
||||
img.alt = "Annotated Image";
|
||||
img.style.maxWidth = "100%";
|
||||
previewArea.appendChild(img);
|
||||
} else {
|
||||
const video = document.createElement('video');
|
||||
video.src = url;
|
||||
video.controls = true;
|
||||
video.style.maxWidth = "100%";
|
||||
previewArea.appendChild(video);
|
||||
}
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
aiInput.value = '';
|
||||
fileInput.value = '';
|
||||
previewArea.innerHTML = '<p>请输入图片URL或上传图片</p>';
|
||||
resultArea.innerHTML = '<h3>检测结果</h3>';
|
||||
loadingIndicator.style.display = 'none';
|
||||
enableAllButtons();
|
||||
}
|
||||
|
||||
function disableAllButtons() {
|
||||
poseButton.disabled = true;
|
||||
fallButton.disabled = true;
|
||||
cpmButton.disabled = true;
|
||||
objectButton.disabled = true;
|
||||
faceButton.disabled = true;
|
||||
qwenvlButton.disabled = true;
|
||||
mediapipeButton.disabled = true;
|
||||
takePictureButton.disabled = true;
|
||||
recordVideoButton.disabled = true;
|
||||
compareButton.disabled = true;
|
||||
qwenvlanalyzeButton.disabled =true;
|
||||
cpmanalyzeButton.disabled = true
|
||||
}
|
||||
|
||||
function enableAllButtons() {
|
||||
poseButton.disabled = false;
|
||||
fallButton.disabled = false;
|
||||
cpmButton.disabled = false;
|
||||
objectButton.disabled = false;
|
||||
faceButton.disabled = false;
|
||||
qwenvlButton.disabled = false;
|
||||
mediapipeButton.disabled = false;
|
||||
takePictureButton.disabled = false;
|
||||
recordVideoButton.disabled = false;
|
||||
compareButton.disabled = false;
|
||||
qwenvlanalyzeButton.disabled =false;
|
||||
cpmanalyzeButton.disabled = false
|
||||
}
|
||||
|
||||
function takePicture() {
|
||||
navigator.mediaDevices.getUserMedia({ video: true })
|
||||
.then(function(stream) {
|
||||
const video = document.createElement('video');
|
||||
video.srcObject = stream;
|
||||
video.play();
|
||||
|
||||
video.onloadedmetadata = function(e) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
|
||||
previewArea.innerHTML = '';
|
||||
const img = document.createElement('img');
|
||||
img.src = canvas.toDataURL('image/jpeg');
|
||||
previewArea.appendChild(img);
|
||||
|
||||
canvas.toBlob(function(blob) {
|
||||
const file = new File([blob], "camera_photo.jpg", { type: "image/jpeg" });
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
fileInput.files = dataTransfer.files;
|
||||
}, 'image/jpeg');
|
||||
};
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(lang[selectedLang].cameraAccessFailed, err);
|
||||
alert(lang[selectedLang].cameraPermissionDenied);
|
||||
});
|
||||
}
|
||||
|
||||
function startRecording() {
|
||||
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
|
||||
.then(function(stream) {
|
||||
previewArea.innerHTML = '<video id="liveVideo" autoplay muted></video>';
|
||||
const liveVideo = document.getElementById('liveVideo');
|
||||
liveVideo.srcObject = stream;
|
||||
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorder.ondataavailable = function(e) {
|
||||
if (e.data.size > 0) {
|
||||
recordedChunks.push(e.data);
|
||||
}
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = function() {
|
||||
const blob = new Blob(recordedChunks, { type: 'video/webm' });
|
||||
const videoURL = URL.createObjectURL(blob);
|
||||
previewArea.innerHTML = '<video id="recordedVideo" controls></video>';
|
||||
const recordedVideo = document.getElementById('recordedVideo');
|
||||
recordedVideo.src = videoURL;
|
||||
|
||||
const file = new File([blob], "camera_video.webm", { type: "video/webm" });
|
||||
const dataTransfer = new DataTransfer();
|
||||
dataTransfer.items.add(file);
|
||||
fileInput.files = dataTransfer.files;
|
||||
|
||||
stream.getTracks().forEach(track => track.stop());
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
recordVideoButton.style.display = 'none';
|
||||
stopRecordingButton.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
if (mediaRecorder.state === 'recording') {
|
||||
stopRecording();
|
||||
}
|
||||
}, 15000);
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error(lang[selectedLang].cameraAccessFailed, err);
|
||||
alert(lang[selectedLang].cameraPermissionDenied);
|
||||
});
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
||||
mediaRecorder.stop();
|
||||
recordVideoButton.style.display = 'block';
|
||||
stopRecordingButton.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// 最后添加其他按钮的事件监听器
|
||||
if (recordVideoButton) recordVideoButton.addEventListener('click', startRecording);
|
||||
if (stopRecordingButton) stopRecordingButton.addEventListener('click', stopRecording);
|
||||
if (clearButton) clearButton.addEventListener('click', clearAll);
|
||||
if (takePictureButton) takePictureButton.addEventListener('click', takePicture);
|
||||
if (poseButton) poseButton.addEventListener('click', () => uploadFile(TASK_TYPES.POSE));
|
||||
if (fallButton) fallButton.addEventListener('click', () => uploadFile(TASK_TYPES.FALL));
|
||||
if (objectButton) objectButton.addEventListener('click', () => uploadFile(TASK_TYPES.YOLO));
|
||||
if (faceButton) faceButton.addEventListener('click', () => uploadFile(TASK_TYPES.FACE));
|
||||
if (compareButton) compareButton.addEventListener('click', () => uploadFile(TASK_TYPES.COMPARE));
|
||||
if (mediapipeButton) mediapipeButton.addEventListener('click', () => uploadFile(TASK_TYPES.MEDIAPIPE));
|
||||
if (cpmButton) cpmButton.addEventListener('click', () => uploadFile(TASK_TYPES.CPM));
|
||||
if (qwenvlButton) qwenvlButton.addEventListener('click', () => uploadFile(TASK_TYPES.QWENVL));
|
||||
if (qwenvlanalyzeButton) qwenvlanalyzeButton.addEventListener('click', () => uploadFile(TASK_TYPES.QWENVL_ANALYZE));
|
||||
if (cpmanalyzeButton) cpmanalyzeButton.addEventListener('click', () => uploadFile(TASK_TYPES.CPM_ANALYZE));
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 获取用户信息的函数
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 944 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,22 @@
|
||||
DB_USER=minio
|
||||
DB_PASSWORD=pif2M_v95k_qT5t
|
||||
DB_HOST=sh-cdb-4ln6r8y0.sql.tencentcdb.com
|
||||
DB_NAME=minio
|
||||
DB_PORT=28234
|
||||
|
||||
# Redis 配置
|
||||
REDIS_HOST=10.0.4.17
|
||||
REDIS_PORT=13003
|
||||
REDIS_DB=0
|
||||
REDIS_API_DB=2
|
||||
REDIS_API_USAGE_DB=3
|
||||
REDIS_PASSWORD=Obscura@2024
|
||||
REDIS_REGISTER_DB=1
|
||||
|
||||
SMTP_SERVER = "smtp.feishu.cn"
|
||||
SMTP_PORT = 465
|
||||
SMTP_USERNAME = "dev@obscura.work"
|
||||
SMTP_PASSWORD = "95RsVmJUAqgwzJDV"
|
||||
|
||||
SUPABASE_URL=https://ihrczmpgwyiqjgsdsxkr.supabase.co
|
||||
SUPABASE_SERVICE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImlocmN6bXBnd3lpcWpnc2RzeGtyIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTczMTYzNDg2MSwiZXhwIjoyMDQ3MjEwODYxfQ.uaySGQTyYRgQYCoQJCXAK5AYNqVjfbjGmvd-ko3C8Ww
|
||||
@@ -0,0 +1,10 @@
|
||||
fastapi
|
||||
sqlalchemy
|
||||
passlib
|
||||
python-dotenv
|
||||
redis
|
||||
uvicorn
|
||||
pydantic[email]
|
||||
python-multipart
|
||||
cryptography
|
||||
supabase
|
||||