sdk_generate/web/static/js/app.js

957 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ============================================================
// 状态管理
// ============================================================
const state = {
docs: [],
page: 1,
size: 10,
total: 0,
currentInstanceId: null,
history: [],
historyPage: 1,
historySize: 10,
historyTotal: 0,
pollInterval: null,
currentTaskId: null,
genInterfaces: [],
};
// ============================================================
// DOM 引用
// ============================================================
const docList = document.getElementById('docList');
const listCount = document.getElementById('listCount');
const pagination = document.getElementById('pagination');
const pageInfo = document.getElementById('pageInfo');
const prevPage = document.getElementById('prevPage');
const nextPage = document.getElementById('nextPage');
// 添加文档
const addDocBtn = document.getElementById('addDocBtn');
const addDocModal = document.getElementById('addDocModal');
const addDocModalClose = document.getElementById('addDocModalClose');
const addCancelBtn = document.getElementById('addCancelBtn');
const addSubmitBtn = document.getElementById('addSubmitBtn');
const addForm = document.getElementById('addDocForm');
const addUploadZone = document.getElementById('addUploadZone');
const addFileInput = document.getElementById('addFileInput');
const addFileInfo = document.getElementById('addFileInfo');
const addFileName = document.getElementById('addFileName');
const addRemoveFile = document.getElementById('addRemoveFile');
// 修改文档
const editDocModal = document.getElementById('editDocModal');
const editDocModalClose = document.getElementById('editDocModalClose');
const editCancelBtn = document.getElementById('editCancelBtn');
const editSubmitBtn = document.getElementById('editSubmitBtn');
const editForm = document.getElementById('editDocForm');
const editInstanceId = document.getElementById('editInstanceId');
const editDesc = document.getElementById('editDesc');
const editRefinedDocRaw = document.getElementById('editRefinedDocRaw');
const editRefinedDocPreview = document.getElementById('editRefinedDocPreview');
const editInterfaceList = document.getElementById('editInterfaceList');
const toggleMarkdownView = document.getElementById('toggleMarkdownView');
// 生成代码
const generateModal = document.getElementById('generateModal');
const generateModalClose = document.getElementById('generateModalClose');
const genCancelBtn = document.getElementById('genCancelBtn');
const genSubmitBtn = document.getElementById('genSubmitBtn');
const genForm = document.getElementById('generateForm');
const genInstanceId = document.getElementById('genInstanceId');
const genInterfacesList = document.getElementById('genInterfacesList');
const genInstanceIdHidden = document.getElementById('genInstanceIdHidden');
const genCodeType = document.getElementById('genCodeType');
const genSdkName = document.getElementById('genSdkName');
const genGenerateType = document.getElementById('genGenerateType');
const genApiKey = document.getElementById('genApiKey');
const genBaseUrl = document.getElementById('genBaseUrl');
const genModel = document.getElementById('genModel');
const genMsgContent = document.getElementById('genMsgContent');
const genMesSendToken = document.getElementById('genMesSendToken');
// 历史任务
const historyModal = document.getElementById('historyModal');
const historyModalClose = document.getElementById('historyModalClose');
const historyInstanceId = document.getElementById('historyInstanceId');
const historyList = document.getElementById('historyList');
const historyPagination = document.getElementById('historyPagination');
const historyPageInfo = document.getElementById('historyPageInfo');
const historyPrevPage = document.getElementById('historyPrevPage');
const historyNextPage = document.getElementById('historyNextPage');
const refreshHistoryBtn = document.getElementById('refreshHistoryBtn');
// 任务详情
const taskDetailModal = document.getElementById('taskDetailModal');
const taskDetailModalClose = document.getElementById('taskDetailModalClose');
const taskDetailId = document.getElementById('taskDetailId');
const taskDetailContent = document.getElementById('taskDetailContent');
// 用量详情
const usageModal = document.getElementById('usageModal');
const usageModalClose = document.getElementById('usageModalClose');
const usageContent = document.getElementById('usageContent');
// 刷新
const refreshListBtn = document.getElementById('refreshListBtn');
// ============================================================
// 工具函数
// ============================================================
function formatTime(isoStr) {
if (!isoStr) return '-';
const d = new Date(isoStr);
return d.toLocaleString('zh-CN');
}
function getGenerateTypeLabel(type) {
const map = {
1: '客户端 SDK',
2: '服务端骨架',
};
return map[type] || type || '-';
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function parseInterfaces(interfaceStr) {
if (!interfaceStr) return [];
try {
return typeof interfaceStr === 'string' ? JSON.parse(interfaceStr) : interfaceStr;
} catch {
return [];
}
}
// ============================================================
// 文档列表
// ============================================================
async function loadDocs(page = state.page) {
try {
const formData = new URLSearchParams();
formData.append('page', String(page));
formData.append('page_size', String(state.size));
const response = await fetch('/api/v1/refine/list', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData.toString(),
});
const data = await response.json();
if (data.code !== 200) {
throw new Error(data.message || '加载失败');
}
state.docs = data.data.data || [];
state.total = data.data.total || 0;
state.page = page;
renderDocs();
renderPagination();
} catch (err) {
console.error('加载列表失败:', err);
docList.innerHTML = `<div class="list-empty">加载失败: ${err.message}</div>`;
}
}
function renderDocs() {
if (state.docs.length === 0) {
docList.innerHTML = '<div class="list-empty">暂无文档实例,点击「添加文档实例」创建</div>';
listCount.textContent = '共 0 个';
return;
}
let html = '';
state.docs.forEach(doc => {
const interfaces = parseInterfaces(doc.interface);
const interfaceTags = interfaces.map(i =>
`<span class="tag">${i.method || 'POST'} ${escapeHtml(i.summary || '')}</span>`
).join('');
html += `
<div class="doc-item" data-instance-id="${doc.instance_id}">
<div class="doc-info">
<div class="doc-name">${escapeHtml(doc.desc || '未命名')}</div>
<div class="doc-meta">
<span>🆔 ${doc.instance_id}</span>
<span>📅 ${formatTime(doc.created_at)}</span>
<span>📊 ${interfaces.length} 个接口</span>
<span>${doc.status === 1 ? '✅ 有效' : '❌ 无效'}</span>
</div>
<div class="doc-interfaces">${interfaceTags}</div>
</div>
<div class="doc-actions">
<button class="btn-edit" data-instance-id="${doc.instance_id}">✏️ 修改</button>
<button class="btn-generate" data-instance-id="${doc.instance_id}">🚀 生成</button>
<button class="btn-history" data-instance-id="${doc.instance_id}">📋 历史</button>
</div>
</div>
`;
});
docList.innerHTML = html;
listCount.textContent = `${state.total}`;
docList.querySelectorAll('.btn-edit').forEach(btn => {
btn.addEventListener('click', () => openEditModal(btn.dataset.instanceId));
});
docList.querySelectorAll('.btn-generate').forEach(btn => {
btn.addEventListener('click', () => openGenerateModal(btn.dataset.instanceId));
});
docList.querySelectorAll('.btn-history').forEach(btn => {
btn.addEventListener('click', () => openHistoryModal(btn.dataset.instanceId));
});
}
function renderPagination() {
const totalPages = Math.ceil(state.total / state.size);
if (totalPages <= 1) {
pagination.style.display = 'none';
return;
}
pagination.style.display = 'flex';
pageInfo.textContent = `${state.page} / ${totalPages}`;
prevPage.disabled = state.page <= 1;
nextPage.disabled = state.page >= totalPages;
}
prevPage.addEventListener('click', () => {
if (state.page > 1) loadDocs(state.page - 1);
});
nextPage.addEventListener('click', () => {
const totalPages = Math.ceil(state.total / state.size);
if (state.page < totalPages) loadDocs(state.page + 1);
});
refreshListBtn.addEventListener('click', () => loadDocs(state.page));
// ============================================================
// 添加文档实例
// ============================================================
let addFileData = null;
addDocBtn.addEventListener('click', () => {
addFileData = null;
addFileInfo.style.display = 'none';
addUploadZone.style.display = 'block';
addFileInput.value = '';
addForm.reset();
addSubmitBtn.disabled = false;
addSubmitBtn.querySelector('.btn-text').textContent = '📝 精炼并保存';
addSubmitBtn.querySelector('.btn-spinner').style.display = 'none';
addDocModal.style.display = 'flex';
});
addDocModalClose.addEventListener('click', () => { addDocModal.style.display = 'none'; });
addCancelBtn.addEventListener('click', () => { addDocModal.style.display = 'none'; });
addUploadZone.addEventListener('click', () => addFileInput.click());
addUploadZone.addEventListener('dragover', (e) => { e.preventDefault(); addUploadZone.classList.add('dragover'); });
addUploadZone.addEventListener('dragleave', () => { addUploadZone.classList.remove('dragover'); });
addUploadZone.addEventListener('drop', (e) => {
e.preventDefault();
addUploadZone.classList.remove('dragover');
if (e.dataTransfer.files.length > 0) {
handleAddFile(e.dataTransfer.files[0]);
}
});
addFileInput.addEventListener('change', (e) => {
if (e.target.files.length > 0) {
handleAddFile(e.target.files[0]);
}
});
addRemoveFile.addEventListener('click', () => {
addFileData = null;
addFileInfo.style.display = 'none';
addUploadZone.style.display = 'block';
addFileInput.value = '';
});
function handleAddFile(file) {
const validTypes = ['.md', '.txt', '.doc', '.docx', '.pdf'];
const ext = '.' + file.name.split('.').pop().toLowerCase();
if (!validTypes.includes(ext)) {
alert('不支持的文件类型');
return;
}
if (file.size > 50 * 1024 * 1024) {
alert('文件大小超过 50MB');
return;
}
addFileData = file;
addFileName.textContent = file.name;
addFileInfo.style.display = 'flex';
addUploadZone.style.display = 'none';
}
addForm.addEventListener('submit', async (e) => {
e.preventDefault();
if (!addFileData) {
alert('请上传文档文件');
return;
}
const apiKey = document.getElementById('addApiKey').value.trim();
const baseUrl = document.getElementById('addBaseUrl').value.trim();
const model = document.getElementById('addModel').value.trim();
const desc = document.getElementById('addDesc').value.trim();
if (!apiKey) { alert('请输入 API Key'); return; }
if (!baseUrl) { alert('请输入 Base URL'); return; }
if (!model) { alert('请输入大模型名称'); return; }
addSubmitBtn.disabled = true;
addSubmitBtn.querySelector('.btn-text').textContent = '精炼中...';
addSubmitBtn.querySelector('.btn-spinner').style.display = 'inline-block';
const formData = new FormData();
formData.append('file', addFileData);
formData.append('llm_api_key', apiKey);
formData.append('llm_base_url', baseUrl);
formData.append('llm_model', model);
formData.append('desc', desc || addFileData.name);
try {
const response = await fetch('/api/v1/refine', {
method: 'POST',
body: formData,
});
const data = await response.json();
if (data.code !== 200) {
throw new Error(data.message || '精炼失败');
}
alert('✅ 文档精炼成功!');
addDocModal.style.display = 'none';
loadDocs(state.page);
} catch (err) {
alert('❌ ' + err.message);
} finally {
addSubmitBtn.disabled = false;
addSubmitBtn.querySelector('.btn-text').textContent = '📝 精炼并保存';
addSubmitBtn.querySelector('.btn-spinner').style.display = 'none';
}
});
// ============================================================
// 修改文档接口列表只读展示Markdown 渲染)
// ============================================================
let editInterfaces = [];
let showRawMarkdown = false;
async function openEditModal(instanceId) {
try {
const formData = new URLSearchParams();
formData.append('page', '1');
formData.append('page_size', '100');
formData.append('instance_id', instanceId);
const response = await fetch('/api/v1/refine/list', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData.toString(),
});
const data = await response.json();
if (data.code !== 200 || !data.data.data || data.data.data.length === 0) {
throw new Error('文档不存在');
}
const doc = data.data.data[0];
state.currentInstanceId = instanceId;
editInstanceId.textContent = 'ID: ' + instanceId;
editDesc.value = doc.desc || '';
// 接口列表只读展示
editInterfaces = parseInterfaces(doc.interface);
renderEditInterfaces();
// Markdown 渲染精炼文档
const refinedDoc = doc.refined_doc || '';
editRefinedDocPreview.innerHTML = renderMarkdown(refinedDoc);
editRefinedDocRaw.value = refinedDoc;
// 默认显示预览,隐藏源码
showRawMarkdown = false;
editRefinedDocPreview.style.display = 'block';
editRefinedDocRaw.style.display = 'none';
if (toggleMarkdownView) {
toggleMarkdownView.textContent = '✏️ 修改';
}
editDocModal.style.display = 'flex';
} catch (err) {
alert('加载文档失败: ' + err.message);
}
}
function renderEditInterfaces() {
const container = editInterfaceList;
container.innerHTML = '';
if (!editInterfaces || editInterfaces.length === 0) {
container.innerHTML = '<span style="color:#9ca3af;font-size:13px;">暂无接口</span>';
return;
}
editInterfaces.forEach(iface => {
const tag = document.createElement('span');
tag.className = 'tag';
tag.innerHTML = `
<span class="method">${escapeHtml(iface.method || 'POST')}</span>
${escapeHtml(iface.summary || '')}
`;
container.appendChild(tag);
});
}
// Markdown 渲染(支持标题、代码块、表格、列表)
function renderMarkdown(text) {
if (!text) return '<span style="color:#9ca3af;">暂无内容</span>';
let html = text;
// 代码块 ``` ... ```
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (match, lang, code) => {
return `<pre><code class="language-${lang}">${escapeHtml(code)}</code></pre>`;
});
// 标题
html = html.replace(/^### (.*$)/gm, '<h3>$1</h3>');
html = html.replace(/^## (.*$)/gm, '<h2>$1</h2>');
html = html.replace(/^# (.*$)/gm, '<h1>$1</h1>');
// 表格
html = html.replace(/\|(.+)\|\n\|[-:| ]+\|\n((?:\|.+\|\n?)*)/g, (match, header, rows) => {
const headers = header.split('|').filter(h => h.trim());
const headerHtml = `<thead><tr>${headers.map(h => `<th>${h.trim()}</th>`).join('')}</tr></thead>`;
const bodyRows = rows.split('\n').filter(r => r.trim()).map(row => {
const cells = row.split('|').filter(c => c.trim());
return `<tr>${cells.map(c => `<td>${c.trim()}</td>`).join('')}</tr>`;
});
return `<table>${headerHtml}<tbody>${bodyRows.join('')}</tbody></table>`;
});
// 列表
html = html.replace(/^- (.*$)/gm, '<li>$1</li>');
html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
// 粗体
html = html.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
// 换行
html = html.replace(/\n/g, '<br>');
return html;
}
// 切换 Markdown 源码/预览(修改按钮)
if (toggleMarkdownView) {
toggleMarkdownView.addEventListener('click', function() {
showRawMarkdown = !showRawMarkdown;
if (showRawMarkdown) {
editRefinedDocPreview.style.display = 'none';
editRefinedDocRaw.style.display = 'block';
this.textContent = '📄 预览';
} else {
editRefinedDocPreview.style.display = 'block';
editRefinedDocRaw.style.display = 'none';
// 重新渲染预览
editRefinedDocPreview.innerHTML = renderMarkdown(editRefinedDocRaw.value);
this.textContent = '✏️ 修改';
}
});
}
editDocModalClose.addEventListener('click', () => { editDocModal.style.display = 'none'; });
editCancelBtn.addEventListener('click', () => { editDocModal.style.display = 'none'; });
editForm.addEventListener('submit', async (e) => {
e.preventDefault();
const refinedDoc = editRefinedDocRaw.value.trim();
const desc = editDesc.value.trim();
editSubmitBtn.disabled = true;
editSubmitBtn.querySelector('.btn-text').textContent = '保存中...';
editSubmitBtn.querySelector('.btn-spinner').style.display = 'inline-block';
try {
const formData = new URLSearchParams();
formData.append('instance_id', state.currentInstanceId);
if (refinedDoc) formData.append('refined_doc', refinedDoc);
if (desc) formData.append('desc', desc);
const response = await fetch('/api/v1/refine/update', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData.toString(),
});
const data = await response.json();
if (data.code !== 200) {
throw new Error(data.message || '保存失败');
}
alert('✅ 保存成功!');
editDocModal.style.display = 'none';
loadDocs(state.page);
} catch (err) {
alert('❌ ' + err.message);
} finally {
editSubmitBtn.disabled = false;
editSubmitBtn.querySelector('.btn-text').textContent = '💾 保存修改';
editSubmitBtn.querySelector('.btn-spinner').style.display = 'none';
}
});
// ============================================================
// 生成代码(接口可勾选,默认全选)
// ============================================================
async function openGenerateModal(instanceId) {
state.currentInstanceId = instanceId;
genInstanceId.textContent = 'ID: ' + instanceId;
genInstanceIdHidden.value = instanceId;
genCodeType.value = 'go';
genForm.reset();
genSubmitBtn.disabled = false;
genSubmitBtn.querySelector('.btn-text').textContent = '🚀 生成代码';
genSubmitBtn.querySelector('.btn-spinner').style.display = 'none';
// 加载接口列表(可勾选)
try {
const formData = new URLSearchParams();
formData.append('page', '1');
formData.append('page_size', '100');
formData.append('instance_id', instanceId);
const response = await fetch('/api/v1/refine/list', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData.toString(),
});
const data = await response.json();
const container = genInterfacesList;
container.innerHTML = '';
if (data.code === 200 && data.data.data && data.data.data.length > 0) {
const doc = data.data.data[0];
const interfaces = parseInterfaces(doc.interface);
state.genInterfaces = interfaces;
if (interfaces.length === 0) {
container.innerHTML = '<span style="color:#9ca3af;font-size:13px;">暂无接口</span>';
} else {
// 默认全选
interfaces.forEach((iface, index) => {
const label = document.createElement('label');
label.className = 'checkbox-item checked';
label.innerHTML = `
<input type="checkbox" checked data-index="${index}" />
<span class="method-tag">${escapeHtml(iface.method || 'POST')}</span>
<span class="summary-text">${escapeHtml(iface.summary || '')}</span>
`;
const checkbox = label.querySelector('input[type="checkbox"]');
checkbox.addEventListener('change', function() {
label.classList.toggle('checked', this.checked);
});
container.appendChild(label);
});
}
} else {
container.innerHTML = '<span style="color:#9ca3af;font-size:13px;">加载接口列表失败</span>';
}
} catch (err) {
console.error('加载接口列表失败:', err);
genInterfacesList.innerHTML = '<span style="color:#9ca3af;font-size:13px;">加载接口列表失败</span>';
}
generateModal.style.display = 'flex';
}
generateModalClose.addEventListener('click', () => { generateModal.style.display = 'none'; });
genCancelBtn.addEventListener('click', () => { generateModal.style.display = 'none'; });
genForm.addEventListener('submit', async (e) => {
e.preventDefault();
const instanceId = genInstanceIdHidden.value;
const sdkName = genSdkName.value.trim();
const generateType = genGenerateType.value;
const apiKey = genApiKey.value.trim();
const baseUrl = genBaseUrl.value.trim();
const model = genModel.value.trim();
const msgContent = genMsgContent.value.trim();
const mesSendToken = genMesSendToken.value.trim();
// 获取选中的接口
const checkboxes = document.querySelectorAll('#genInterfacesList input[type="checkbox"]:checked');
const selectedInterfaces = [];
checkboxes.forEach(cb => {
const idx = parseInt(cb.dataset.index);
if (state.genInterfaces && state.genInterfaces[idx]) {
selectedInterfaces.push(state.genInterfaces[idx]);
}
});
if (selectedInterfaces.length === 0) {
alert('请至少选择一个接口');
return;
}
if (!instanceId) { alert('文档实例ID不能为空'); return; }
if (!sdkName) { alert('请输入仓库名称'); return; }
if (!apiKey) { alert('请输入 API Key'); return; }
if (!baseUrl) { alert('请输入 Base URL'); return; }
if (!model) { alert('请输入大模型名称'); return; }
genSubmitBtn.disabled = true;
genSubmitBtn.querySelector('.btn-text').textContent = '提交中...';
genSubmitBtn.querySelector('.btn-spinner').style.display = 'inline-block';
try {
const formData = new URLSearchParams();
formData.append('instance_id', instanceId);
formData.append('llm_api_key', apiKey);
formData.append('llm_base_url', baseUrl);
formData.append('llm_model', model);
formData.append('desc', sdkName);
formData.append('code_type', 'go');
formData.append('generate_type', generateType);
formData.append('interfaces', JSON.stringify(selectedInterfaces));
if (msgContent) formData.append('msg_content', msgContent);
if (mesSendToken) formData.append('mes_send_token', mesSendToken);
const response = await fetch('/api/v1/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData.toString(),
});
const data = await response.json();
if (data.code !== 200) {
throw new Error(data.message || '提交失败');
}
const taskId = data.data?.task_id || data.task_id;
alert(`✅ 任务已提交任务ID: ${taskId}`);
generateModal.style.display = 'none';
loadDocs(state.page);
} catch (err) {
alert('❌ ' + err.message);
} finally {
genSubmitBtn.disabled = false;
genSubmitBtn.querySelector('.btn-text').textContent = '🚀 生成代码';
genSubmitBtn.querySelector('.btn-spinner').style.display = 'none';
}
});
// ============================================================
// 历史任务列表(美化版)
// ============================================================
async function openHistoryModal(instanceId) {
state.currentInstanceId = instanceId;
historyInstanceId.textContent = 'ID: ' + instanceId;
state.historyPage = 1;
historyModal.style.display = 'flex';
await loadHistory(instanceId, 1);
}
historyModalClose.addEventListener('click', () => { historyModal.style.display = 'none'; });
refreshHistoryBtn.addEventListener('click', () => {
if (state.currentInstanceId) {
loadHistory(state.currentInstanceId, state.historyPage);
}
});
async function loadHistory(instanceId, page = 1) {
try {
const formData = new URLSearchParams();
formData.append('page', String(page));
formData.append('page_size', String(state.historySize));
formData.append('task_id', instanceId);
const response = await fetch('/api/v1/tasks/list', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: formData.toString(),
});
const data = await response.json();
if (data.code !== 200) {
throw new Error(data.message || '加载失败');
}
state.history = data.data.data || [];
state.historyTotal = data.data.total || 0;
state.historyPage = page;
renderHistory();
} catch (err) {
historyList.innerHTML = `<div class="list-empty">加载失败: ${err.message}</div>`;
}
}
function renderHistory() {
if (state.history.length === 0) {
historyList.innerHTML = '<div class="list-empty">暂无历史任务</div>';
historyPagination.style.display = 'none';
return;
}
let html = '';
state.history.forEach(task => {
const statusClass = task.task_status === 'completed' ? 'completed' :
task.task_status === 'failed' ? 'failed' :
task.task_status === 'pending' ? 'pending' : 'processing';
const interfaces = parseInterfaces(task.interfaces);
let usageSummary = '';
let usageDetail = null;
try {
const ua = typeof task.use_age === 'string' ? JSON.parse(task.use_age) : task.use_age;
if (ua) {
usageSummary = `总: ${ua.total_tokens || 0}`;
usageDetail = ua;
}
} catch {}
html += `
<div class="history-item" data-task-id="${task.task_id}" data-usage='${JSON.stringify(usageDetail || {})}'>
<div class="h-info">
<div class="h-id">${task.task_id}</div>
<div class="h-meta">
<span>📅 ${formatTime(task.created_at)}</span>
<span>📦 ${escapeHtml(task.desc || '-')}</span>
<span>🤖 ${escapeHtml(task.llm_model || '-')}</span>
<span>📊 ${interfaces.length} 个接口</span>
<span>📋 ${getGenerateTypeLabel(task.generate_type)}</span>
<span>💻 ${escapeHtml(task.code_type || 'go')}</span>
${task.repo_url ? `<span>🔗 <a href="${task.repo_url}" target="_blank" onclick="event.stopPropagation();">仓库</a></span>` : ''}
${usageSummary ? `<span class="usage-badge" onclick="event.stopPropagation();showUsageDetail(this)">📊 ${usageSummary}</span>` : ''}
</div>
</div>
<div class="h-status ${statusClass}">${task.task_status}</div>
</div>
`;
});
historyList.innerHTML = html;
historyList.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', () => {
const taskId = item.dataset.taskId;
openTaskDetail(taskId);
});
});
const totalPages = Math.ceil(state.historyTotal / state.historySize);
if (totalPages <= 1) {
historyPagination.style.display = 'none';
} else {
historyPagination.style.display = 'flex';
historyPageInfo.textContent = `${state.historyPage} / ${totalPages}`;
historyPrevPage.disabled = state.historyPage <= 1;
historyNextPage.disabled = state.historyPage >= totalPages;
}
}
historyPrevPage.addEventListener('click', () => {
if (state.historyPage > 1 && state.currentInstanceId) {
loadHistory(state.currentInstanceId, state.historyPage - 1);
}
});
historyNextPage.addEventListener('click', () => {
const totalPages = Math.ceil(state.historyTotal / state.historySize);
if (state.historyPage < totalPages && state.currentInstanceId) {
loadHistory(state.currentInstanceId, state.historyPage + 1);
}
});
// ============================================================
// 用量详情弹窗(美化版)
// ============================================================
function showUsageDetail(el) {
const item = el.closest('.history-item');
if (!item) return;
const usageData = JSON.parse(item.dataset.usage || '{}');
const content = usageContent;
if (!usageData || Object.keys(usageData).length === 0 || usageData.total_tokens === 0) {
content.innerHTML = '<div style="text-align:center;color:#9ca3af;padding:20px;">暂无用量数据</div>';
usageModal.style.display = 'flex';
return;
}
const promptTokens = usageData.prompt_tokens || 0;
const completionTokens = usageData.completion_tokens || 0;
const totalTokens = usageData.total_tokens || 0;
const detail = usageData.detail || [];
let detailHtml = '';
if (detail.length > 0) {
detailHtml = `
<div style="margin-top:16px;">
<div style="font-size:13px;font-weight:600;color:#374151;margin-bottom:8px;">分步详情</div>
<div class="usage-detail-list">
${detail.map(d => `
<div class="usage-detail-item">
<span class="step-name">${escapeHtml(d.state_name || '未知步骤')}</span>
<span class="step-tokens">${d.total_tokens || 0} tokens</span>
</div>
`).join('')}
</div>
</div>
`;
}
content.innerHTML = `
<div class="usage-stats">
<div class="usage-stat-card">
<div class="stat-value">${totalTokens}</div>
<div class="stat-label">总 Tokens</div>
</div>
<div class="usage-stat-card">
<div class="stat-value">${promptTokens}</div>
<div class="stat-label">输入 Tokens <span class="sub">(Prompt)</span></div>
</div>
<div class="usage-stat-card">
<div class="stat-value">${completionTokens}</div>
<div class="stat-label">输出 Tokens <span class="sub">(Completion)</span></div>
</div>
</div>
${detailHtml}
`;
usageModal.style.display = 'flex';
}
usageModalClose.addEventListener('click', () => {
usageModal.style.display = 'none';
});
// 点击弹窗外关闭
usageModal.addEventListener('click', (e) => {
if (e.target === usageModal) {
usageModal.style.display = 'none';
}
});
// ============================================================
// 任务详情
// ============================================================
async function openTaskDetail(taskId) {
taskDetailId.textContent = 'ID: ' + taskId;
taskDetailContent.innerHTML = '<div class="detail-loading">加载中...</div>';
taskDetailModal.style.display = 'flex';
try {
const response = await fetch(`/api/v1/tasks/${taskId}`);
const data = await response.json();
if (data.code !== 200) {
throw new Error(data.message || '加载失败');
}
const task = data.data?.Task || data.data || {};
renderTaskDetail(task);
} catch (err) {
taskDetailContent.innerHTML = `<div class="list-empty">加载失败: ${err.message}</div>`;
}
}
function renderTaskDetail(task) {
const interfaces = parseInterfaces(task.interfaces);
const interfaceTags = interfaces.map(i =>
`<span class="tag">${i.method || 'POST'} ${escapeHtml(i.summary || '')}</span>`
).join('');
let useAge = '';
try {
const ua = typeof task.use_age === 'string' ? JSON.parse(task.use_age) : task.use_age;
if (ua) {
useAge = `
<div style="margin-top:6px;font-size:13px;color:#6b7280;">
总 Tokens: ${ua.total_tokens || 0}
${ua.detail ? ua.detail.map(d =>
`<span style="margin-left:12px;">${d.state_name || '未知'}: ${d.total_tokens || 0}</span>`
).join('') : ''}
</div>
`;
}
} catch {}
taskDetailContent.innerHTML = `
<div class="detail-row">
<span class="label">任务ID</span>
<span class="value">${task.task_id}</span>
</div>
<div class="detail-row">
<span class="label">状态</span>
<span class="value"><span class="h-status ${task.task_status === 'completed' ? 'completed' : task.task_status === 'failed' ? 'failed' : 'processing'}">${task.task_status}</span></span>
</div>
<div class="detail-row">
<span class="label">仓库名称</span>
<span class="value">${escapeHtml(task.desc || '-')}</span>
</div>
<div class="detail-row">
<span class="label">生成类型</span>
<span class="value">${getGenerateTypeLabel(task.generate_type)}</span>
</div>
<div class="detail-row">
<span class="label">代码类型</span>
<span class="value">${escapeHtml(task.code_type || 'go')}</span>
</div>
<div class="detail-row">
<span class="label">大模型</span>
<span class="value">${escapeHtml(task.llm_model || '-')}</span>
</div>
<div class="detail-row">
<span class="label">接口列表</span>
<span class="value">${interfaceTags || '-'}</span>
</div>
<div class="detail-row">
<span class="label">创建时间</span>
<span class="value">${formatTime(task.created_at)}</span>
</div>
<div class="detail-row">
<span class="label">完成时间</span>
<span class="value">${formatTime(task.completed_at)}</span>
</div>
<div class="detail-row">
<span class="label">代码仓库</span>
<span class="value">${task.repo_url ? `<a href="${task.repo_url}" target="_blank">${task.repo_url}</a>` : '-'}</span>
</div>
<div class="detail-row">
<span class="label">错误信息</span>
<span class="value" style="color:#dc2626;">${escapeHtml(task.error || '-')}</span>
</div>
<div class="detail-row">
<span class="label">Token 用量</span>
<span class="value">${useAge || '-'}</span>
</div>
`;
}
taskDetailModalClose.addEventListener('click', () => { taskDetailModal.style.display = 'none'; });
// ============================================================
// 初始化
// ============================================================
loadDocs(1);