475 lines
15 KiB
JavaScript
475 lines
15 KiB
JavaScript
// InsightFlow Frontend - Phase 6 (API Platform)
|
|
const API_BASE = '/api/v1';
|
|
|
|
let currentProject = null;
|
|
let currentData = null;
|
|
let selectedEntity = null;
|
|
let projectRelations = [];
|
|
let projectEntities = [];
|
|
let entityDetailsCache = {};
|
|
|
|
// Graph Analysis State
|
|
let graphStats = null;
|
|
let centralityData = null;
|
|
let communitiesData = null;
|
|
let currentPathData = null;
|
|
|
|
// Init
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const isWorkbench = window.location.pathname.includes('workbench');
|
|
if (isWorkbench) {
|
|
initWorkbench();
|
|
}
|
|
});
|
|
|
|
// Initialize workbench
|
|
async function initWorkbench() {
|
|
const projectId = localStorage.getItem('currentProject');
|
|
if (!projectId) {
|
|
window.location.href = '/';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const projects = await fetchProjects();
|
|
currentProject = projects.find(p => p.id === projectId);
|
|
|
|
if (!currentProject) {
|
|
localStorage.removeItem('currentProject');
|
|
window.location.href = '/';
|
|
return;
|
|
}
|
|
|
|
const nameEl = document.getElementById('projectName');
|
|
if (nameEl) nameEl.textContent = currentProject.name;
|
|
|
|
initUpload();
|
|
initAgentPanel();
|
|
initEntityCard();
|
|
await loadProjectData();
|
|
|
|
} catch (err) {
|
|
console.error('Init failed:', err);
|
|
alert('加载失败,请返回项目列表');
|
|
}
|
|
}
|
|
|
|
// ==================== API Calls ====================
|
|
|
|
async function fetchProjects() {
|
|
const res = await fetch(`${API_BASE}/projects`);
|
|
if (!res.ok) throw new Error('Failed to fetch projects');
|
|
return await res.json();
|
|
}
|
|
|
|
async function uploadAudio(file) {
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const res = await fetch(`${API_BASE}/projects/${currentProject.id}/upload`, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
if (!res.ok) throw new Error('Upload failed');
|
|
return await res.json();
|
|
}
|
|
|
|
async function loadProjectData() {
|
|
try {
|
|
const [entitiesRes, relationsRes] = await Promise.all([
|
|
fetch(`${API_BASE}/projects/${currentProject.id}/entities`),
|
|
fetch(`${API_BASE}/projects/${currentProject.id}/relations`)
|
|
]);
|
|
|
|
if (entitiesRes.ok) {
|
|
projectEntities = await entitiesRes.json();
|
|
}
|
|
if (relationsRes.ok) {
|
|
projectRelations = await relationsRes.json();
|
|
}
|
|
|
|
// 预加载实体详情
|
|
await preloadEntityDetails();
|
|
|
|
currentData = {
|
|
transcript_id: 'project_view',
|
|
project_id: currentProject.id,
|
|
segments: [],
|
|
entities: projectEntities,
|
|
full_text: '',
|
|
relations: projectRelations
|
|
};
|
|
|
|
renderTranscript();
|
|
renderGraph();
|
|
renderEntityList();
|
|
|
|
} catch (err) {
|
|
console.error('Load project data failed:', err);
|
|
}
|
|
}
|
|
|
|
async function preloadEntityDetails() {
|
|
const promises = projectEntities.slice(0, 20).map(async entity => {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/entities/${entity.id}/details`);
|
|
if (res.ok) {
|
|
entityDetailsCache[entity.id] = await res.json();
|
|
}
|
|
} catch (e) {
|
|
// Ignore errors
|
|
}
|
|
});
|
|
await Promise.all(promises);
|
|
}
|
|
|
|
// ==================== View Switching ====================
|
|
|
|
window.switchView = function(viewName) {
|
|
// Update sidebar buttons
|
|
document.querySelectorAll('.sidebar-btn').forEach(btn => {
|
|
btn.classList.remove('active');
|
|
});
|
|
|
|
const views = {
|
|
'workbench': 'workbenchView',
|
|
'knowledge-base': 'knowledgeBaseView',
|
|
'timeline': 'timelineView',
|
|
'reasoning': 'reasoningView',
|
|
'graph-analysis': 'graphAnalysisView',
|
|
'api-keys': 'apiKeysView'
|
|
};
|
|
|
|
// Hide all views
|
|
Object.values(views).forEach(id => {
|
|
const el = document.getElementById(id);
|
|
if (el) {
|
|
el.style.display = 'none';
|
|
el.classList.remove('active', 'show');
|
|
}
|
|
});
|
|
|
|
// Show selected view
|
|
const targetId = views[viewName];
|
|
if (targetId) {
|
|
const targetEl = document.getElementById(targetId);
|
|
if (targetEl) {
|
|
targetEl.style.display = 'flex';
|
|
targetEl.classList.add('active', 'show');
|
|
}
|
|
}
|
|
|
|
// Update active button
|
|
const btnMap = {
|
|
'workbench': 0,
|
|
'knowledge-base': 1,
|
|
'timeline': 2,
|
|
'reasoning': 3,
|
|
'graph-analysis': 4,
|
|
'api-keys': 5
|
|
};
|
|
const buttons = document.querySelectorAll('.sidebar-btn');
|
|
if (buttons[btnMap[viewName]]) {
|
|
buttons[btnMap[viewName]].classList.add('active');
|
|
}
|
|
|
|
// Load view-specific data
|
|
if (viewName === 'knowledge-base') {
|
|
loadKnowledgeBase();
|
|
} else if (viewName === 'timeline') {
|
|
loadTimeline();
|
|
} else if (viewName === 'graph-analysis') {
|
|
initGraphAnalysis();
|
|
} else if (viewName === 'api-keys') {
|
|
loadApiKeys();
|
|
}
|
|
};
|
|
|
|
// ==================== Phase 6: API Key Management ====================
|
|
|
|
let apiKeysData = [];
|
|
let currentApiKeyId = null;
|
|
|
|
// Load API Keys
|
|
async function loadApiKeys() {
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api-keys`);
|
|
if (!res.ok) throw new Error('Failed to fetch API keys');
|
|
const data = await res.json();
|
|
apiKeysData = data.keys || [];
|
|
renderApiKeys();
|
|
updateApiKeyStats();
|
|
} catch (err) {
|
|
console.error('Failed to load API keys:', err);
|
|
document.getElementById('apiKeysListContent').innerHTML = `
|
|
<div class="api-key-empty">
|
|
<p>加载失败: ${err.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Update API Key Stats
|
|
function updateApiKeyStats() {
|
|
const total = apiKeysData.length;
|
|
const active = apiKeysData.filter(k => k.status === 'active').length;
|
|
const revoked = apiKeysData.filter(k => k.status === 'revoked').length;
|
|
const totalCalls = apiKeysData.reduce((sum, k) => sum + (k.total_calls || 0), 0);
|
|
|
|
document.getElementById('apiKeyTotalCount').textContent = total;
|
|
document.getElementById('apiKeyActiveCount').textContent = active;
|
|
document.getElementById('apiKeyRevokedCount').textContent = revoked;
|
|
document.getElementById('apiKeyTotalCalls').textContent = totalCalls.toLocaleString();
|
|
}
|
|
|
|
// Render API Keys List
|
|
function renderApiKeys() {
|
|
const container = document.getElementById('apiKeysListContent');
|
|
|
|
if (apiKeysData.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="api-key-empty">
|
|
<p>暂无 API Keys</p>
|
|
<button class="btn btn-small" onclick="showCreateApiKeyModal()" style="margin-top:12px;">创建第一个 API Key</button>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = apiKeysData.map(key => `
|
|
<div class="api-key-item">
|
|
<div>
|
|
<div class="api-key-name">${escapeHtml(key.name)}</div>
|
|
<div class="api-key-preview">${key.key_preview}</div>
|
|
</div>
|
|
<div class="api-key-permissions">
|
|
${key.permissions.map(p => `<span class="api-key-permission ${p}">${p}</span>`).join('')}
|
|
</div>
|
|
<div>${key.rate_limit}/min</div>
|
|
<div>
|
|
<span class="api-key-status ${key.status}">${key.status}</span>
|
|
</div>
|
|
<div>${key.total_calls || 0}</div>
|
|
<div class="api-key-actions">
|
|
${key.status === 'active' ? `
|
|
<button class="api-key-btn" onclick="showApiKeyStats('${key.id}', '${escapeHtml(key.name)}')">统计</button>
|
|
<button class="api-key-btn danger" onclick="revokeApiKey('${key.id}')">撤销</button>
|
|
` : '<span style="color:#666;font-size:0.8rem;">已失效</span>'}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Show Create API Key Modal
|
|
window.showCreateApiKeyModal = function() {
|
|
document.getElementById('apiKeyCreateModal').classList.add('show');
|
|
document.getElementById('apiKeyName').value = '';
|
|
document.getElementById('apiKeyName').focus();
|
|
};
|
|
|
|
// Hide Create API Key Modal
|
|
window.hideCreateApiKeyModal = function() {
|
|
document.getElementById('apiKeyCreateModal').classList.remove('show');
|
|
};
|
|
|
|
// Create API Key
|
|
window.createApiKey = async function() {
|
|
const name = document.getElementById('apiKeyName').value.trim();
|
|
if (!name) {
|
|
alert('请输入 API Key 名称');
|
|
return;
|
|
}
|
|
|
|
const permissions = [];
|
|
if (document.getElementById('permRead').checked) permissions.push('read');
|
|
if (document.getElementById('permWrite').checked) permissions.push('write');
|
|
if (document.getElementById('permDelete').checked) permissions.push('delete');
|
|
|
|
if (permissions.length === 0) {
|
|
alert('请至少选择一个权限');
|
|
return;
|
|
}
|
|
|
|
const rateLimit = parseInt(document.getElementById('apiKeyRateLimit').value);
|
|
const expiresDays = document.getElementById('apiKeyExpires').value;
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api-keys`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name,
|
|
permissions,
|
|
rate_limit: rateLimit,
|
|
expires_days: expiresDays ? parseInt(expiresDays) : null
|
|
})
|
|
});
|
|
|
|
if (!res.ok) throw new Error('Failed to create API key');
|
|
|
|
const data = await res.json();
|
|
hideCreateApiKeyModal();
|
|
|
|
// Show the created key
|
|
document.getElementById('createdApiKeyValue').textContent = data.api_key;
|
|
document.getElementById('apiKeyCreatedModal').classList.add('show');
|
|
|
|
// Refresh list
|
|
await loadApiKeys();
|
|
} catch (err) {
|
|
console.error('Failed to create API key:', err);
|
|
alert('创建失败: ' + err.message);
|
|
}
|
|
};
|
|
|
|
// Copy API Key to clipboard
|
|
window.copyApiKey = function() {
|
|
const key = document.getElementById('createdApiKeyValue').textContent;
|
|
navigator.clipboard.writeText(key).then(() => {
|
|
showNotification('API Key 已复制到剪贴板', 'success');
|
|
}).catch(() => {
|
|
// Fallback
|
|
const textarea = document.createElement('textarea');
|
|
textarea.value = key;
|
|
document.body.appendChild(textarea);
|
|
textarea.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(textarea);
|
|
showNotification('API Key 已复制到剪贴板', 'success');
|
|
});
|
|
};
|
|
|
|
// Hide API Key Created Modal
|
|
window.hideApiKeyCreatedModal = function() {
|
|
document.getElementById('apiKeyCreatedModal').classList.remove('show');
|
|
};
|
|
|
|
// Show API Key Stats
|
|
window.showApiKeyStats = async function(keyId, keyName) {
|
|
currentApiKeyId = keyId;
|
|
document.getElementById('apiKeyStatsTitle').textContent = `API Key 统计 - ${keyName}`;
|
|
document.getElementById('apiKeyStatsModal').classList.add('show');
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api-keys/${keyId}/stats?days=30`);
|
|
if (!res.ok) throw new Error('Failed to fetch stats');
|
|
|
|
const data = await res.json();
|
|
|
|
// Update stats
|
|
document.getElementById('statsTotalCalls').textContent = data.summary.total_calls.toLocaleString();
|
|
document.getElementById('statsSuccessCalls').textContent = data.summary.success_calls.toLocaleString();
|
|
document.getElementById('statsErrorCalls').textContent = data.summary.error_calls.toLocaleString();
|
|
document.getElementById('statsAvgTime').textContent = Math.round(data.summary.avg_response_time_ms);
|
|
|
|
// Render logs
|
|
renderApiKeyLogs(data.logs || []);
|
|
} catch (err) {
|
|
console.error('Failed to load stats:', err);
|
|
document.getElementById('apiKeyLogs').innerHTML = `
|
|
<div class="api-key-empty">
|
|
<p>加载统计失败</p>
|
|
</div>
|
|
`;
|
|
}
|
|
};
|
|
|
|
// Render API Key Logs
|
|
function renderApiKeyLogs(logs) {
|
|
const container = document.getElementById('apiKeyLogs');
|
|
|
|
if (logs.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="api-key-empty">
|
|
<p>暂无调用记录</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = logs.map(log => `
|
|
<div class="api-key-log-item">
|
|
<div class="api-key-log-endpoint" title="${escapeHtml(log.endpoint)}">${escapeHtml(log.endpoint)}</div>
|
|
<div class="api-key-log-method">${log.method}</div>
|
|
<div class="api-key-log-status ${log.status_code < 400 ? 'success' : 'error'}">${log.status_code}</div>
|
|
<div class="api-key-log-time">${log.response_time_ms}ms</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Hide API Key Stats Modal
|
|
window.hideApiKeyStatsModal = function() {
|
|
document.getElementById('apiKeyStatsModal').classList.remove('show');
|
|
currentApiKeyId = null;
|
|
};
|
|
|
|
// Revoke API Key
|
|
window.revokeApiKey = async function(keyId) {
|
|
if (!confirm('确定要撤销此 API Key 吗?撤销后将无法恢复。')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/api-keys/${keyId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (!res.ok) throw new Error('Failed to revoke API key');
|
|
|
|
showNotification('API Key 已撤销', 'success');
|
|
await loadApiKeys();
|
|
} catch (err) {
|
|
console.error('Failed to revoke API key:', err);
|
|
alert('撤销失败: ' + err.message);
|
|
}
|
|
};
|
|
|
|
// Escape HTML helper
|
|
function escapeHtml(text) {
|
|
if (!text) return '';
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
// Show notification helper
|
|
function showNotification(message, type = 'info') {
|
|
const notification = document.createElement('div');
|
|
notification.style.cssText = `
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: ${type === 'success' ? 'rgba(0, 212, 255, 0.9)' : type === 'error' ? 'rgba(255, 107, 107, 0.9)' : '#333'};
|
|
color: ${type === 'success' || type === 'error' ? '#000' : '#fff'};
|
|
padding: 12px 20px;
|
|
border-radius: 8px;
|
|
z-index: 10000;
|
|
font-size: 0.9rem;
|
|
animation: slideIn 0.3s ease;
|
|
`;
|
|
notification.textContent = message;
|
|
|
|
document.body.appendChild(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.style.animation = 'slideOut 0.3s ease';
|
|
setTimeout(() => {
|
|
if (notification.parentNode) {
|
|
document.body.removeChild(notification);
|
|
}
|
|
}, 300);
|
|
}, 3000);
|
|
}
|
|
|
|
// Placeholder functions for other views
|
|
function initUpload() {}
|
|
function initAgentPanel() {}
|
|
function initEntityCard() {}
|
|
function renderTranscript() {}
|
|
function renderGraph() {}
|
|
function renderEntityList() {}
|
|
function loadKnowledgeBase() {}
|
|
function loadTimeline() {}
|
|
function initGraphAnalysis() {}
|