sdk_generate/web/static/js/app.js

496 lines
15 KiB
JavaScript

// ============================================================
// 状态管理
// ============================================================
const state = {
file: null,
taskId: null,
status: 'idle',
pollInterval: null,
currentStatusName: '',
repoURL: '',
dotCount: 0,
dotInterval: null,
progressSimInterval: null,
};
// ============================================================
// DOM 引用
// ============================================================
const uploadZone = document.getElementById('uploadZone');
const fileInput = document.getElementById('fileInput');
const fileInfo = document.getElementById('fileInfo');
const fileName = document.getElementById('fileName');
const fileSize = document.getElementById('fileSize');
const removeFile = document.getElementById('removeFile');
const sdkName = document.getElementById('sdkName');
const llmModel = document.getElementById('llmModel');
const apiKey = document.getElementById('apiKey');
const baseUrl = document.getElementById('baseUrl');
const generateBtn = document.getElementById('generateBtn');
const btnText = document.querySelector('.btn-text');
const btnSpinner = document.querySelector('.btn-spinner');
const progressSection = document.getElementById('progressSection');
const progressFill = document.getElementById('progressFill');
const progressStatus = document.getElementById('progressStatus');
const progressLogs = document.getElementById('progressLogs');
const resultSection = document.getElementById('resultSection');
const resultFiles = document.getElementById('resultFiles');
const resultError = document.getElementById('resultError');
const downloadBtn = document.getElementById('downloadBtn');
const viewRepoBtn = document.getElementById('viewRepoBtn');
// ============================================================
// 动态效果函数
// ============================================================
// 1. 进度条闪烁动画
function addProgressGlow() {
const existingStyle = document.getElementById('progressGlowStyle');
if (existingStyle) return;
const style = document.createElement('style');
style.id = 'progressGlowStyle';
style.textContent = `
.progress-fill.glow {
background: linear-gradient(90deg, #6366f1, #8b5cf6, #6366f1);
background-size: 200% 100%;
animation: glowMove 1.5s ease-in-out infinite;
position: relative;
}
.progress-fill.glow::after {
content: '';
position: absolute;
right: -4px;
top: -4px;
width: 14px;
height: 14px;
background: #8b5cf6;
border-radius: 50%;
box-shadow: 0 0 20px 8px rgba(139, 92, 246, 0.6);
animation: pulse 1s ease-in-out infinite;
}
@keyframes glowMove {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.3); opacity: 0.6; }
}
`;
document.head.appendChild(style);
}
// 2. 状态文字动态点点点
function startDotAnimation() {
if (state.dotInterval) {
clearInterval(state.dotInterval);
}
let dotCount = 0;
state.dotInterval = setInterval(() => {
dotCount = (dotCount + 1) % 4;
const dots = '.'.repeat(dotCount);
if (state.status === 'generating' || state.status === 'pending') {
const currentText = progressStatus.textContent.replace(/\.\.\.$/, '').trim();
if (!currentText.includes('✅') && !currentText.includes('❌')) {
progressStatus.textContent = currentText + dots;
}
}
}, 500);
}
function stopDotAnimation() {
if (state.dotInterval) {
clearInterval(state.dotInterval);
state.dotInterval = null;
}
}
// 3. 进度条模拟前进
function startProgressSimulation() {
if (state.progressSimInterval) {
clearInterval(state.progressSimInterval);
}
let lastPercent = parseInt(progressFill.style.width) || 0;
state.progressSimInterval = setInterval(() => {
if (state.status === 'generating' && lastPercent < 95) {
const increment = 0.5 + Math.random() * 1.5;
lastPercent = Math.min(lastPercent + increment, 95);
progressFill.style.width = lastPercent + '%';
}
}, 2000);
}
function stopProgressSimulation() {
if (state.progressSimInterval) {
clearInterval(state.progressSimInterval);
state.progressSimInterval = null;
}
}
// ============================================================
// 日志管理(只让最新的一条闪烁)
// ============================================================
let logCounter = 0;
function addLog(type, message) {
const line = document.createElement('div');
line.className = `log-line ${type}`;
line.textContent = message;
progressLogs.appendChild(line);
progressLogs.scrollTop = progressLogs.scrollHeight;
// 移除之前所有日志的闪烁效果
document.querySelectorAll('.log-line .log-dot-blink').forEach(el => {
el.classList.remove('log-dot-blink');
});
// 为最新日志添加闪烁点(在行首添加)
const dotSpan = document.createElement('span');
dotSpan.className = 'log-dot-blink';
dotSpan.textContent = '● ';
dotSpan.style.cssText = `
color: #60a5fa;
animation: logBlink 1s ease-in-out infinite;
`;
line.prepend(dotSpan);
// 确保动画样式存在
const styleId = 'logBlinkStyle';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
@keyframes logBlink {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.2; transform: scale(0.8); }
}
`;
document.head.appendChild(style);
}
}
function clearLogs() {
progressLogs.innerHTML = '';
logCounter = 0;
}
// ============================================================
// 上传区域事件
// ============================================================
uploadZone.addEventListener('click', () => fileInput.click());
uploadZone.addEventListener('dragover', (e) => {
e.preventDefault();
uploadZone.classList.add('dragover');
});
uploadZone.addEventListener('dragleave', () => {
uploadZone.classList.remove('dragover');
});
uploadZone.addEventListener('drop', (e) => {
e.preventDefault();
uploadZone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
handleFile(e.dataTransfer.files[0]);
}
});
fileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleFile(e.target.files[0]);
}
});
removeFile.addEventListener('click', () => {
state.file = null;
fileInfo.style.display = 'none';
uploadZone.style.display = 'block';
generateBtn.disabled = true;
fileInput.value = '';
});
// ============================================================
// 文件处理
// ============================================================
function handleFile(file) {
const validTypes = ['.md', '.txt', '.doc', '.docx', '.pdf'];
const ext = '.' + file.name.split('.').pop().toLowerCase();
if (!validTypes.includes(ext)) {
alert('不支持的文件类型,请上传 .md .txt .doc .docx .pdf');
return;
}
if (file.size > 50 * 1024 * 1024) {
alert('文件大小超过 50MB 限制');
return;
}
state.file = file;
fileName.textContent = file.name;
fileSize.textContent = formatFileSize(file.size);
fileInfo.style.display = 'flex';
uploadZone.style.display = 'none';
generateBtn.disabled = false;
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
// ============================================================
// 生成按钮
// ============================================================
generateBtn.addEventListener('click', generateSDK);
async function generateSDK() {
if (!state.file) return;
const sdkNameVal = sdkName.value.trim();
const modelVal = llmModel.value.trim();
const apiKeyVal = apiKey.value.trim();
const baseUrlVal = baseUrl.value.trim();
if (!sdkNameVal) {
alert('请输入 SDK 名称');
sdkName.focus();
return;
}
if (!modelVal) {
alert('请输入大模型名称');
llmModel.focus();
return;
}
if (!apiKeyVal) {
alert('请输入 API Key');
apiKey.focus();
return;
}
if (!baseUrlVal) {
alert('请输入 Base URL');
baseUrl.focus();
return;
}
// 重置状态
state.status = 'generating';
state.taskId = null;
state.currentStatusName = '';
state.repoURL = '';
resultSection.style.display = 'none';
resultError.style.display = 'none';
resultFiles.innerHTML = '';
progressSection.style.display = 'block';
progressFill.style.width = '0%';
progressStatus.textContent = '准备中...';
clearLogs();
generateBtn.disabled = true;
btnText.textContent = '生成中';
btnSpinner.style.display = 'inline-block';
viewRepoBtn.style.display = 'none';
// 启动动态效果
addProgressGlow();
progressFill.classList.add('glow');
startDotAnimation();
startProgressSimulation();
const formData = new FormData();
formData.append('file', state.file);
formData.append('sdk_name', sdkNameVal);
formData.append('llm_model', modelVal);
formData.append('llm_api_key', apiKeyVal);
formData.append('llm_base_url', baseUrlVal);
try {
const response = await fetch('/api/v1/generate', {
method: 'POST',
body: formData,
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || '生成失败');
}
state.taskId = data.task_id;
addLog('info', '📤 任务已提交: ' + data.task_id);
pollTaskStatus();
} catch (err) {
showError(err.message);
}
}
// ============================================================
// 轮询任务状态
// ============================================================
function pollTaskStatus() {
if (state.pollInterval) {
clearInterval(state.pollInterval);
}
state.pollInterval = setInterval(async () => {
try {
const response = await fetch(`/api/v1/tasks/${state.taskId}`);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || '查询状态失败');
}
updateProgress(data);
if (data.status === 'generateComplete' || data.status === 'completed') {
clearInterval(state.pollInterval);
state.pollInterval = null;
onComplete(data);
} else if (data.status === 'failed') {
clearInterval(state.pollInterval);
state.pollInterval = null;
showError(data.error || '生成失败');
}
} catch (err) {
clearInterval(state.pollInterval);
state.pollInterval = null;
showError(err.message);
}
}, 2000);
}
function updateProgress(data) {
const percent = data.percent || 0;
progressFill.style.width = percent + '%';
const statusName = data.status_name || '';
if (statusName && statusName !== state.currentStatusName) {
state.currentStatusName = statusName;
addLog('info', '🔄 ' + statusName);
}
// ✅ 更新状态文字(带闪烁前缀)
const statusTextMap = {
'pending': '⏳ 等待中',
'generateCode': '🔄 生成代码',
'validateCode': '🔍 验证代码',
'generateComplete': '✅ 完成!',
'completed': '✅ 完成!',
'failed': '❌ 失败',
};
const baseText = statusTextMap[data.status] || (statusName || data.status);
// ✅ 只有进行中的状态才让文字闪烁
const isProcessing = data.status !== 'generateComplete' && data.status !== 'completed' && data.status !== 'failed';
if (isProcessing) {
progressStatus.innerHTML = `<span class="status-blink">●</span> ${baseText}`;
} else {
progressStatus.textContent = baseText;
stopDotAnimation();
}
// ✅ 确保闪烁样式存在
const styleId = 'statusBlinkStyle';
if (!document.getElementById(styleId) && isProcessing) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
.status-blink {
color: #60a5fa;
display: inline-block;
animation: statusBlink 1s ease-in-out infinite;
margin-right: 6px;
}
@keyframes statusBlink {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.2; transform: scale(0.8); }
}
`;
document.head.appendChild(style);
}
}
// ============================================================
// 完成处理
// ============================================================
function onComplete(data) {
stopDotAnimation();
stopProgressSimulation();
progressFill.classList.remove('glow');
generateBtn.disabled = false;
btnText.textContent = '生成 SDK';
btnSpinner.style.display = 'none';
resultSection.style.display = 'block';
resultFiles.innerHTML = '';
const viewBtn = document.getElementById('viewBtn');
if (viewBtn) {
viewBtn.style.display = 'none';
}
downloadBtn.onclick = () => {
window.location.href = `/api/v1/tasks/${state.taskId}/download`;
};
const repoURL = data.RepoURL || data.repo_url || '';
if (repoURL) {
viewRepoBtn.style.display = 'inline-block';
viewRepoBtn.onclick = () => {
window.open(repoURL, '_blank');
};
} else {
viewRepoBtn.style.display = 'none';
}
// ✅ 完成状态不带闪烁
progressStatus.textContent = '✅ 生成完成!';
progressFill.style.width = '100%';
addLog('success', '🎉 SDK 生成完成!');
}
// ============================================================
// 错误处理
// ============================================================
function showError(message) {
stopDotAnimation();
stopProgressSimulation();
progressFill.classList.remove('glow');
state.status = 'error';
generateBtn.disabled = false;
btnText.textContent = '生成 SDK';
btnSpinner.style.display = 'none';
resultSection.style.display = 'block';
resultError.style.display = 'block';
resultError.textContent = '❌ ' + message;
viewRepoBtn.style.display = 'none';
// ✅ 错误状态不带闪烁
progressStatus.textContent = '❌ 失败';
progressFill.style.width = '100%';
progressFill.style.background = '#f87171';
if (state.pollInterval) {
clearInterval(state.pollInterval);
state.pollInterval = null;
}
}