496 lines
15 KiB
JavaScript
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;
|
|
}
|
|
} |