// InsightFlow Frontend - Phase 4 (Agent Assistant + Provenance) const API_BASE = '/api/v1'; let currentProject = null; let currentData = null; let selectedEntity = null; let projectRelations = []; let projectEntities = []; let entityDetailsCache = {}; // 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: '', created_at: new Date().toISOString() }; renderGraph(); renderEntityList(); } catch (err) { console.error('Load project data failed:', err); } } async function preloadEntityDetails() { // 并行加载所有实体详情 const promises = projectEntities.map(async (ent) => { try { const res = await fetch(`${API_BASE}/entities/${ent.id}/details`); if (res.ok) { entityDetailsCache[ent.id] = await res.json(); } } catch (e) { console.error(`Failed to load entity ${ent.id} details:`, e); } }); await Promise.all(promises); } // ==================== Agent Panel ==================== function initAgentPanel() { const chatInput = document.getElementById('chatInput'); if (chatInput) { chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendAgentMessage(); } }); } } function toggleAgentPanel() { const panel = document.getElementById('agentPanel'); const toggle = panel.querySelector('.agent-toggle'); panel.classList.toggle('collapsed'); toggle.textContent = panel.classList.contains('collapsed') ? '‹' : '›'; } function addChatMessage(content, isUser = false, isTyping = false) { const container = document.getElementById('chatMessages'); const msgDiv = document.createElement('div'); msgDiv.className = `chat-message ${isUser ? 'user' : 'assistant'}`; if (isTyping) { msgDiv.innerHTML = `
`; } else { msgDiv.innerHTML = `
${content}
`; } container.appendChild(msgDiv); container.scrollTop = container.scrollHeight; return msgDiv; } function removeTypingIndicator() { const indicator = document.getElementById('typingIndicator'); if (indicator) { indicator.parentElement.remove(); } } async function sendAgentMessage() { const input = document.getElementById('chatInput'); const message = input.value.trim(); if (!message) return; input.value = ''; addChatMessage(message, true); addChatMessage('', false, true); try { // 判断是命令还是问答 const isCommand = message.includes('合并') || message.includes('修改') || message.startsWith('把') || message.startsWith('将'); if (isCommand) { // 执行命令 const res = await fetch(`${API_BASE}/projects/${currentProject.id}/agent/command`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: message }) }); removeTypingIndicator(); if (res.ok) { const result = await res.json(); let response = ''; if (result.intent === 'merge_entities') { if (result.success) { response = `✅ 已合并 ${result.merged.length} 个实体到 "${result.target}"`; await loadProjectData(); // 刷新数据 } else { response = `❌ 合并失败:${result.error || '未找到匹配的实体'}`; } } else if (result.intent === 'edit_entity') { if (result.success) { response = `✅ 已更新实体 "${result.entity?.name}"`; await loadProjectData(); } else { response = `❌ 编辑失败:${result.error || '未找到实体'}`; } } else if (result.intent === 'answer_question') { response = result.answer; } else { response = result.message || result.explanation || '未识别的指令'; } addChatMessage(response); } else { addChatMessage('❌ 请求失败,请重试'); } } else { // RAG 问答 const res = await fetch(`${API_BASE}/projects/${currentProject.id}/agent/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: message, stream: false }) }); removeTypingIndicator(); if (res.ok) { const result = await res.json(); addChatMessage(result.answer); } else { addChatMessage('❌ 获取回答失败,请重试'); } } } catch (err) { removeTypingIndicator(); addChatMessage('❌ 网络错误,请检查连接'); console.error('Agent error:', err); } } async function loadSuggestions() { addChatMessage('正在获取建议...', false, true); try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/agent/suggest`); removeTypingIndicator(); if (res.ok) { const result = await res.json(); const suggestions = result.suggestions || []; if (suggestions.length === 0) { addChatMessage('暂无建议,请先上传一些音频文件。'); return; } let html = '
💡 基于项目数据的建议:
'; suggestions.forEach((s, i) => { html += `
${s.type === 'action' ? '⚡ 操作' : '💡 洞察'}
${s.title}
${s.description}
`; }); const msgDiv = document.createElement('div'); msgDiv.className = 'chat-message assistant'; msgDiv.innerHTML = `
${html}
`; document.getElementById('chatMessages').appendChild(msgDiv); } } catch (err) { removeTypingIndicator(); addChatMessage('❌ 获取建议失败'); } } function applySuggestion(index) { // 可以在这里实现建议的自动应用 addChatMessage('建议功能开发中,敬请期待!'); } // ==================== Transcript Rendering ==================== function renderTranscript() { const container = document.getElementById('transcriptContent'); if (!container || !currentData || !currentData.segments) return; container.innerHTML = ''; currentData.segments.forEach((seg, idx) => { const div = document.createElement('div'); div.className = 'segment'; div.dataset.index = idx; let text = seg.text; const entities = findEntitiesInText(seg.text); entities.sort((a, b) => b.start - a.start); entities.forEach(ent => { const before = text.slice(0, ent.start); const name = text.slice(ent.start, ent.end); const after = text.slice(ent.end); const details = entityDetailsCache[ent.id]; const confidence = details?.mentions?.[0]?.confidence || 1.0; const lowConfClass = confidence < 0.7 ? 'low-confidence' : ''; text = before + `${name}` + after; }); div.innerHTML = `
${seg.speaker}
${text}
`; container.appendChild(div); }); } function findEntitiesInText(text) { if (!projectEntities || projectEntities.length === 0) return []; const found = []; projectEntities.forEach(ent => { const name = ent.name; let pos = 0; while ((pos = text.indexOf(name, pos)) !== -1) { found.push({ id: ent.id, name: ent.name, start: pos, end: pos + name.length }); pos += 1; } if (ent.aliases && ent.aliases.length > 0) { ent.aliases.forEach(alias => { let aliasPos = 0; while ((aliasPos = text.indexOf(alias, aliasPos)) !== -1) { found.push({ id: ent.id, name: alias, start: aliasPos, end: aliasPos + alias.length }); aliasPos += 1; } }); } }); return found; } // ==================== Entity Card ==================== function initEntityCard() { const card = document.getElementById('entityCard'); // 鼠标移出卡片时隐藏 card.addEventListener('mouseleave', () => { card.classList.remove('show'); }); } function showEntityCard(event, entityId) { const card = document.getElementById('entityCard'); const details = entityDetailsCache[entityId]; const entity = projectEntities.find(e => e.id === entityId); if (!entity) return; // 更新卡片内容 document.getElementById('cardName').textContent = entity.name; document.getElementById('cardBadge').textContent = entity.type; document.getElementById('cardBadge').className = `entity-type-badge type-${entity.type.toLowerCase()}`; document.getElementById('cardDefinition').textContent = entity.definition || '暂无定义'; const mentionCount = details?.mentions?.length || 0; const relationCount = details?.relations?.length || 0; document.getElementById('cardMentions').textContent = `${mentionCount} 次提及`; document.getElementById('cardRelations').textContent = `${relationCount} 个关系`; // 定位卡片 const rect = event.target.getBoundingClientRect(); card.style.left = `${rect.left}px`; card.style.top = `${rect.bottom + 10}px`; // 确保不超出屏幕 const cardRect = card.getBoundingClientRect(); if (cardRect.right > window.innerWidth) { card.style.left = `${window.innerWidth - cardRect.width - 20}px`; } card.classList.add('show'); } function hideEntityCard() { // 延迟隐藏,允许鼠标移到卡片上 setTimeout(() => { const card = document.getElementById('entityCard'); if (!card.matches(':hover')) { card.classList.remove('show'); } }, 100); } // ==================== Graph Visualization ==================== function renderGraph() { const svg = d3.select('#graph-svg'); svg.selectAll('*').remove(); if (!projectEntities || projectEntities.length === 0) { svg.append('text') .attr('x', '50%') .attr('y', '50%') .attr('text-anchor', 'middle') .attr('fill', '#666') .text('暂无实体数据,请上传音频'); return; } const container = svg.node().parentElement; const width = container.clientWidth; const height = container.clientHeight - 200; svg.attr('width', width).attr('height', height); const nodes = projectEntities.map(e => ({ id: e.id, name: e.name, type: e.type, definition: e.definition, ...e })); const links = projectRelations.map(r => ({ id: r.id, source: r.source_id, target: r.target_id, type: r.type, evidence: r.evidence })).filter(r => r.source && r.target); if (links.length === 0 && nodes.length > 1) { for (let i = 0; i < Math.min(nodes.length - 1, 5); i++) { links.push({ source: nodes[0].id, target: nodes[i + 1].id, type: 'related' }); } } const colorMap = { 'PROJECT': '#7b2cbf', 'TECH': '#00d4ff', 'PERSON': '#ff6b6b', 'ORG': '#4ecdc4', 'OTHER': '#666' }; const simulation = d3.forceSimulation(nodes) .force('link', d3.forceLink(links).id(d => d.id).distance(120)) .force('charge', d3.forceManyBody().strength(-400)) .force('center', d3.forceCenter(width / 2, height / 2)) .force('collision', d3.forceCollide().radius(50)); // 关系连线 const link = svg.append('g') .selectAll('line') .data(links) .enter().append('line') .attr('stroke', '#444') .attr('stroke-width', 1.5) .attr('stroke-opacity', 0.6) .style('cursor', 'pointer') .on('click', (e, d) => showProvenance(d)); // 关系标签 const linkLabel = svg.append('g') .selectAll('text') .data(links) .enter().append('text') .attr('font-size', '10px') .attr('fill', '#666') .attr('text-anchor', 'middle') .style('pointer-events', 'none') .text(d => d.type); // 节点组 const node = svg.append('g') .selectAll('g') .data(nodes) .enter().append('g') .attr('class', 'node') .call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)) .on('click', (e, d) => window.selectEntity(d.id)) .on('mouseenter', (e, d) => showEntityCard(e, d.id)) .on('mouseleave', hideEntityCard); // 节点圆圈 node.append('circle') .attr('r', 35) .attr('fill', d => colorMap[d.type] || '#666') .attr('stroke', '#fff') .attr('stroke-width', 2) .attr('class', 'node-circle'); // 节点文字 node.append('text') .text(d => d.name.length > 6 ? d.name.slice(0, 5) + '...' : d.name) .attr('text-anchor', 'middle') .attr('dy', 5) .attr('fill', '#fff') .attr('font-size', '11px') .attr('font-weight', '500') .style('pointer-events', 'none'); // 节点类型图标 node.append('text') .attr('dy', -45) .attr('text-anchor', 'middle') .attr('fill', d => colorMap[d.type] || '#666') .attr('font-size', '10px') .text(d => d.type) .style('pointer-events', 'none'); simulation.on('tick', () => { link .attr('x1', d => d.source.x) .attr('y1', d => d.source.y) .attr('x2', d => d.target.x) .attr('y2', d => d.target.y); linkLabel .attr('x', d => (d.source.x + d.target.x) / 2) .attr('y', d => (d.source.y + d.target.y) / 2); node.attr('transform', d => `translate(${d.x},${d.y})`); }); function dragstarted(e, d) { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(e, d) { d.fx = e.x; d.fy = e.y; } function dragended(e, d) { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } } // ==================== Provenance ==================== async function showProvenance(relation) { const modal = document.getElementById('provenanceModal'); const body = document.getElementById('provenanceBody'); modal.classList.add('show'); body.innerHTML = '

加载中...

'; try { let content = ''; if (relation.id) { // 从API获取溯源信息 const res = await fetch(`${API_BASE}/relations/${relation.id}/provenance`); if (res.ok) { const data = await res.json(); content = `
关系类型
${data.source} → ${data.type} → ${data.target}
来源文档
${data.transcript?.filename || '未知文件'}
证据文本
"${data.evidence || '无证据文本'}"
`; } else { content = '

获取溯源信息失败

'; } } else { // 使用本地数据 content = `
关系类型
${relation.source.name || relation.source} → ${relation.type} → ${relation.target.name || relation.target}
证据文本
"${relation.evidence || '无证据文本'}"
`; } body.innerHTML = content; } catch (err) { body.innerHTML = '

加载失败

'; } } function closeProvenance() { document.getElementById('provenanceModal').classList.remove('show'); } // ==================== Entity List ==================== function renderEntityList() { const container = document.getElementById('entityList'); if (!container) return; container.innerHTML = '

项目实体

'; if (!projectEntities || projectEntities.length === 0) { container.innerHTML += '

暂无实体,请上传音频文件

'; return; } projectEntities.forEach(ent => { const div = document.createElement('div'); div.className = 'entity-item'; div.dataset.id = ent.id; div.onclick = () => window.selectEntity(ent.id); div.onmouseenter = (e) => showEntityCard(e, ent.id); div.onmouseleave = hideEntityCard; div.innerHTML = ` ${ent.type}
${ent.name}
${ent.definition || '暂无定义'}
`; container.appendChild(div); }); } // ==================== Entity Selection ==================== window.selectEntity = function(entityId) { selectedEntity = entityId; const entity = projectEntities.find(e => e.id === entityId); if (!entity) return; // 高亮文本中的实体 document.querySelectorAll('.entity').forEach(el => { if (el.dataset.id === entityId) { el.style.background = '#ff6b6b'; el.style.color = '#fff'; } else { el.style.background = ''; el.style.color = ''; } }); // 高亮图谱中的节点 d3.selectAll('.node-circle') .attr('stroke', d => d.id === entityId ? '#ff6b6b' : '#fff') .attr('stroke-width', d => d.id === entityId ? 4 : 2) .attr('r', d => d.id === entityId ? 40 : 35); // 高亮实体列表 document.querySelectorAll('.entity-item').forEach(el => { if (el.dataset.id === entityId) { el.style.background = '#2a2a2a'; el.style.borderLeft = '3px solid #ff6b6b'; } else { el.style.background = ''; el.style.borderLeft = ''; } }); console.log('Selected:', entity.name, entity.definition); }; // ==================== Upload ==================== window.showUpload = function() { const el = document.getElementById('uploadOverlay'); if (el) el.classList.add('show'); }; window.hideUpload = function() { const el = document.getElementById('uploadOverlay'); if (el) el.classList.remove('show'); }; function initUpload() { const input = document.getElementById('fileInput'); const overlay = document.getElementById('uploadOverlay'); if (!input) return; input.addEventListener('change', async (e) => { if (!e.target.files.length) return; const file = e.target.files[0]; if (overlay) { overlay.innerHTML = `

正在分析...

${file.name}

ASR转录 + 实体提取中

`; } try { const result = await uploadAudio(file); currentData = result; await loadProjectData(); if (result.segments && result.segments.length > 0) { renderTranscript(); } if (overlay) overlay.classList.remove('show'); } catch (err) { console.error('Upload failed:', err); if (overlay) { overlay.innerHTML = `

分析失败

${err.message}

`; } } }); } // ==================== Phase 5: Timeline View ==================== async function loadTimeline() { const container = document.getElementById('timelineContainer'); const entityFilter = document.getElementById('timelineEntityFilter'); if (!container) return; container.innerHTML = '

加载时间线数据...

'; try { // 更新实体筛选器选项 if (entityFilter && projectEntities.length > 0) { const currentValue = entityFilter.value; entityFilter.innerHTML = ''; projectEntities.forEach(ent => { const option = document.createElement('option'); option.value = ent.id; option.textContent = ent.name; entityFilter.appendChild(option); }); entityFilter.value = currentValue; } // 构建查询参数 const params = new URLSearchParams(); if (entityFilter && entityFilter.value) { params.append('entity_id', entityFilter.value); } // 获取时间线数据 const res = await fetch(`${API_BASE}/projects/${currentProject.id}/timeline?${params}`); if (!res.ok) throw new Error('Failed to load timeline'); const data = await res.json(); const events = data.events || []; // 更新统计 const mentions = events.filter(e => e.type === 'mention').length; const relations = events.filter(e => e.type === 'relation').length; document.getElementById('timelineTotalEvents').textContent = events.length; document.getElementById('timelineMentions').textContent = mentions; document.getElementById('timelineRelations').textContent = relations; // 渲染时间线 renderTimeline(events); } catch (err) { console.error('Load timeline failed:', err); container.innerHTML = '

加载失败,请重试

'; } } function renderTimeline(events) { const container = document.getElementById('timelineContainer'); if (events.length === 0) { container.innerHTML = `

暂无时间线数据

请先上传音频或文档文件

`; return; } // 按日期分组 const grouped = groupEventsByDate(events); let html = '
'; Object.entries(grouped).forEach(([date, dayEvents]) => { const dateLabel = formatDateLabel(date); html += `
${dateLabel}
`; dayEvents.forEach(event => { html += renderTimelineEvent(event); }); html += '
'; }); container.innerHTML = html; } function groupEventsByDate(events) { const grouped = {}; events.forEach(event => { const date = event.event_date.split('T')[0]; if (!grouped[date]) { grouped[date] = []; } grouped[date].push(event); }); return grouped; } function formatDateLabel(dateStr) { const date = new Date(dateStr); const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); if (dateStr === today.toISOString().split('T')[0]) { return '今天'; } else if (dateStr === yesterday.toISOString().split('T')[0]) { return '昨天'; } else { return `${date.getMonth() + 1}月${date.getDate()}日`; } } function renderTimelineEvent(event) { if (event.type === 'mention') { return `
提及 ${event.entity_name} ${event.entity_type || 'OTHER'}
"${event.text_snippet || ''}"
📄 ${event.source?.filename || '未知文件'} ${event.confidence ? `置信度: ${(event.confidence * 100).toFixed(0)}%` : ''}
`; } else if (event.type === 'relation') { return `
关系 ${event.source_entity} → ${event.target_entity}
关系类型: ${event.relation_type}
${event.evidence ? `
"${event.evidence}"
` : ''}
📄 ${event.source?.filename || '未知文件'}
`; } return ''; } // ==================== View Switching ==================== window.switchView = function(viewName) { // 更新侧边栏按钮状态 document.querySelectorAll('.sidebar-btn').forEach(btn => { btn.classList.remove('active'); }); // 隐藏所有视图 document.getElementById('workbenchView').style.display = 'none'; document.getElementById('knowledgeBaseView').classList.remove('show'); document.getElementById('timelineView').classList.remove('show'); document.getElementById('reasoningView').classList.remove('active'); // 显示选中的视图 if (viewName === 'workbench') { document.getElementById('workbenchView').style.display = 'flex'; document.querySelector('.sidebar-btn:nth-child(1)').classList.add('active'); } else if (viewName === 'knowledge-base') { document.getElementById('knowledgeBaseView').classList.add('show'); document.querySelector('.sidebar-btn:nth-child(2)').classList.add('active'); loadKnowledgeBase(); } else if (viewName === 'timeline') { document.getElementById('timelineView').classList.add('show'); document.querySelector('.sidebar-btn:nth-child(3)').classList.add('active'); loadTimeline(); } else if (viewName === 'reasoning') { document.getElementById('reasoningView').classList.add('active'); document.querySelector('.sidebar-btn:nth-child(4)').classList.add('active'); } }; window.switchKBTab = function(tabName) { // 更新导航项状态 document.querySelectorAll('.kb-nav-item').forEach(item => { item.classList.remove('active'); }); // 隐藏所有部分 document.querySelectorAll('.kb-section').forEach(section => { section.classList.remove('active'); }); // 显示选中的部分 const tabMap = { 'entities': { nav: 0, section: 'kbEntitiesSection' }, 'relations': { nav: 1, section: 'kbRelationsSection' }, 'glossary': { nav: 2, section: 'kbGlossarySection' }, 'transcripts': { nav: 3, section: 'kbTranscriptsSection' } }; const mapping = tabMap[tabName]; if (mapping) { document.querySelectorAll('.kb-nav-item')[mapping.nav].classList.add('active'); document.getElementById(mapping.section).classList.add('active'); } }; async function loadKnowledgeBase() { if (!currentProject) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/knowledge-base`); if (!res.ok) throw new Error('Failed to load knowledge base'); const data = await res.json(); // 更新统计 document.getElementById('kbEntityCount').textContent = data.stats.entity_count; document.getElementById('kbRelationCount').textContent = data.stats.relation_count; document.getElementById('kbTranscriptCount').textContent = data.stats.transcript_count; document.getElementById('kbGlossaryCount').textContent = data.stats.glossary_count; // 渲染实体网格 const entityGrid = document.getElementById('kbEntityGrid'); entityGrid.innerHTML = ''; data.entities.forEach(ent => { const card = document.createElement('div'); card.className = 'kb-entity-card'; card.onclick = () => { switchView('workbench'); setTimeout(() => selectEntity(ent.id), 100); }; // 渲染属性预览 let attrsHtml = ''; if (ent.attributes && ent.attributes.length > 0) { attrsHtml = `
${ent.attributes.slice(0, 3).map(a => ` ${a.name}: ${Array.isArray(a.value) ? a.value.join(', ') : a.value} `).join('')} ${ent.attributes.length > 3 ? `+${ent.attributes.length - 3}` : ''}
`; } card.innerHTML = `
${ent.type} ${ent.name}
${ent.definition || '暂无定义'}
📍 ${ent.mention_count || 0} 次提及 · ${ent.appears_in?.length || 0} 个文件
${attrsHtml} `; entityGrid.appendChild(card); }); // 渲染关系列表 const relationsList = document.getElementById('kbRelationsList'); relationsList.innerHTML = ''; data.relations.forEach(rel => { const item = document.createElement('div'); item.className = 'kb-glossary-item'; item.innerHTML = `
${rel.source_name} → ${rel.type} → ${rel.target_name} ${rel.evidence ? `
"${rel.evidence.substring(0, 100)}..."
` : ''}
`; relationsList.appendChild(item); }); // 渲染术语表 const glossaryList = document.getElementById('kbGlossaryList'); glossaryList.innerHTML = ''; data.glossary.forEach(term => { const item = document.createElement('div'); item.className = 'kb-glossary-item'; item.innerHTML = `
${term.term} ${term.pronunciation ? `(${term.pronunciation})` : ''} 出现 ${term.frequency} 次
`; glossaryList.appendChild(item); }); // 渲染文件列表 const transcriptsList = document.getElementById('kbTranscriptsList'); transcriptsList.innerHTML = ''; data.transcripts.forEach(t => { const item = document.createElement('div'); item.className = 'kb-transcript-item'; item.innerHTML = `
${t.type === 'audio' ? '🎵' : '📄'} ${t.filename}
${new Date(t.created_at).toLocaleString()}
`; transcriptsList.appendChild(item); }); } catch (err) { console.error('Load knowledge base failed:', err); } } // ==================== Glossary Functions ==================== window.showAddTermModal = function() { document.getElementById('glossaryModal').classList.add('show'); }; window.hideGlossaryModal = function() { document.getElementById('glossaryModal').classList.remove('show'); }; window.saveGlossaryTerm = async function() { const term = document.getElementById('glossaryTerm').value.trim(); const pronunciation = document.getElementById('glossaryPronunciation').value.trim(); if (!term) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/glossary`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ term, pronunciation }) }); if (res.ok) { hideGlossaryModal(); document.getElementById('glossaryTerm').value = ''; document.getElementById('glossaryPronunciation').value = ''; loadKnowledgeBase(); } } catch (err) { console.error('Save glossary term failed:', err); } }; window.deleteGlossaryTerm = async function(termId) { if (!confirm('确定要删除这个术语吗?')) return; try { const res = await fetch(`${API_BASE}/glossary/${termId}`, { method: 'DELETE' }); if (res.ok) { loadKnowledgeBase(); } } catch (err) { console.error('Delete glossary term failed:', err); } }; // ==================== Phase 5: Knowledge Reasoning ==================== window.submitReasoningQuery = async function() { const input = document.getElementById('reasoningInput'); const depth = document.getElementById('reasoningDepth').value; const query = input.value.trim(); if (!query) return; const resultsDiv = document.getElementById('reasoningResults'); // 显示加载状态 resultsDiv.innerHTML = `

正在进行知识推理...

`; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/reasoning/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, reasoning_depth: depth }) }); if (!res.ok) throw new Error('Reasoning failed'); const data = await res.json(); renderReasoningResult(data); } catch (err) { console.error('Reasoning query failed:', err); resultsDiv.innerHTML = `

推理失败,请稍后重试

`; } }; function renderReasoningResult(data) { const resultsDiv = document.getElementById('reasoningResults'); const typeLabels = { 'causal': '🔍 因果推理', 'comparative': '⚖️ 对比推理', 'temporal': '⏱️ 时序推理', 'associative': '🔗 关联推理', 'summary': '📝 总结推理' }; const typeLabel = typeLabels[data.reasoning_type] || '🤔 智能分析'; const confidencePercent = Math.round(data.confidence * 100); let evidenceHtml = ''; if (data.evidence && data.evidence.length > 0) { evidenceHtml = `

📋 支撑证据

${data.evidence.map(e => `
${e.text || e}
`).join('')}
`; } let gapsHtml = ''; if (data.knowledge_gaps && data.knowledge_gaps.length > 0) { gapsHtml = `

⚠️ 知识缺口

`; } resultsDiv.innerHTML = `
${typeLabel}
置信度: ${confidencePercent}%
${data.answer.replace(/\n/g, '
')}
${evidenceHtml} ${gapsHtml}
`; } window.clearReasoningResult = function() { document.getElementById('reasoningResults').innerHTML = ''; document.getElementById('reasoningInput').value = ''; document.getElementById('inferencePathsSection').style.display = 'none'; }; window.generateSummary = async function(summaryType) { const resultsDiv = document.getElementById('reasoningResults'); // 显示加载状态 resultsDiv.innerHTML = `

正在生成项目总结...

`; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/reasoning/summary`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ summary_type: summaryType }) }); if (!res.ok) throw new Error('Summary failed'); const data = await res.json(); renderSummaryResult(data); } catch (err) { console.error('Summary generation failed:', err); resultsDiv.innerHTML = `

总结生成失败,请稍后重试

`; } }; function renderSummaryResult(data) { const resultsDiv = document.getElementById('reasoningResults'); const typeLabels = { 'comprehensive': '📋 全面总结', 'executive': '💼 高管摘要', 'technical': '⚙️ 技术总结', 'risk': '⚠️ 风险分析' }; const typeLabel = typeLabels[data.summary_type] || '📝 项目总结'; let keyPointsHtml = ''; if (data.key_points && data.key_points.length > 0) { keyPointsHtml = `

📌 关键要点

`; } let risksHtml = ''; if (data.risks && data.risks.length > 0) { risksHtml = `

⚠️ 风险与问题

`; } let recommendationsHtml = ''; if (data.recommendations && data.recommendations.length > 0) { recommendationsHtml = `

💡 建议

${data.recommendations.map(r => `
${r}
`).join('')}
`; } resultsDiv.innerHTML = `
${typeLabel}
置信度: ${Math.round(data.confidence * 100)}%
${data.overview ? data.overview.replace(/\n/g, '
') : ''}
${keyPointsHtml} ${risksHtml} ${recommendationsHtml}
`; } window.findInferencePath = async function(startEntity, endEntity) { const pathsSection = document.getElementById('inferencePathsSection'); const pathsList = document.getElementById('inferencePathsList'); pathsSection.style.display = 'block'; pathsList.innerHTML = '

正在搜索关联路径...

'; try { const res = await fetch( `${API_BASE}/projects/${currentProject.id}/reasoning/inference-path?start_entity=${encodeURIComponent(startEntity)}&end_entity=${encodeURIComponent(endEntity)}` ); if (!res.ok) throw new Error('Path finding failed'); const data = await res.json(); renderInferencePaths(data); } catch (err) { console.error('Path finding failed:', err); pathsList.innerHTML = '

路径搜索失败

'; } }; // Phase 5: Entity Attributes Management let currentEntityIdForAttributes = null; let currentAttributes = []; let currentTemplates = []; // Show entity attributes modal window.showEntityAttributes = async function(entityId) { if (entityId) { currentEntityIdForAttributes = entityId; } else if (selectedEntity) { currentEntityIdForAttributes = selectedEntity; } else { alert('请先选择一个实体'); return; } const modal = document.getElementById('attributesModal'); modal.classList.add('show'); // Reset form document.getElementById('attributesAddForm').style.display = 'none'; document.getElementById('toggleAddAttrBtn').style.display = 'inline-block'; document.getElementById('saveAttrBtn').style.display = 'none'; await loadEntityAttributes(); }; window.hideAttributesModal = function() { document.getElementById('attributesModal').classList.remove('show'); currentEntityIdForAttributes = null; }; async function loadEntityAttributes() { if (!currentEntityIdForAttributes) return; try { const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes`); if (!res.ok) throw new Error('Failed to load attributes'); const data = await res.json(); currentAttributes = data.attributes || []; renderAttributesList(); } catch (err) { console.error('Load attributes failed:', err); document.getElementById('attributesList').innerHTML = '

加载失败

'; } } function renderAttributesList() { const container = document.getElementById('attributesList'); if (currentAttributes.length === 0) { container.innerHTML = '

暂无属性,点击"添加属性"创建

'; return; } container.innerHTML = currentAttributes.map(attr => { let valueDisplay = attr.value; if (attr.type === 'multiselect' && Array.isArray(attr.value)) { valueDisplay = attr.value.join(', '); } return `
${attr.name} ${attr.type}
${valueDisplay || '-'}
`; }).join(''); } window.toggleAddAttributeForm = function() { const form = document.getElementById('attributesAddForm'); const toggleBtn = document.getElementById('toggleAddAttrBtn'); const saveBtn = document.getElementById('saveAttrBtn'); if (form.style.display === 'none') { form.style.display = 'block'; toggleBtn.style.display = 'none'; saveBtn.style.display = 'inline-block'; } else { form.style.display = 'none'; toggleBtn.style.display = 'inline-block'; saveBtn.style.display = 'none'; } }; window.onAttrTypeChange = function() { const type = document.getElementById('attrType').value; const optionsGroup = document.getElementById('attrOptionsGroup'); const valueContainer = document.getElementById('attrValueContainer'); if (type === 'select' || type === 'multiselect') { optionsGroup.style.display = 'block'; } else { optionsGroup.style.display = 'none'; } // Update value input based on type if (type === 'date') { valueContainer.innerHTML = ''; } else if (type === 'number') { valueContainer.innerHTML = ''; } else { valueContainer.innerHTML = ''; } }; window.saveAttribute = async function() { if (!currentEntityIdForAttributes) return; const name = document.getElementById('attrName').value.trim(); const type = document.getElementById('attrType').value; let value = document.getElementById('attrValue').value; const changeReason = document.getElementById('attrChangeReason').value.trim(); if (!name) { alert('请输入属性名称'); return; } // Handle options for select/multiselect let options = null; if (type === 'select' || type === 'multiselect') { const optionsStr = document.getElementById('attrOptions').value.trim(); if (optionsStr) { options = optionsStr.split(',').map(o => o.trim()).filter(o => o); } // Handle multiselect value if (type === 'multiselect' && value) { value = value.split(',').map(v => v.trim()).filter(v => v); } } // Handle number type if (type === 'number' && value) { value = parseFloat(value); } try { const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, type, value, options, change_reason: changeReason }) }); if (!res.ok) throw new Error('Failed to save attribute'); // Reset form document.getElementById('attrName').value = ''; document.getElementById('attrValue').value = ''; document.getElementById('attrOptions').value = ''; document.getElementById('attrChangeReason').value = ''; // Reload attributes await loadEntityAttributes(); // Hide form toggleAddAttributeForm(); } catch (err) { console.error('Save attribute failed:', err); alert('保存失败,请重试'); } }; window.deleteAttribute = async function(attributeId) { if (!confirm('确定要删除这个属性吗?')) return; try { const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes/${attributeId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete attribute'); await loadEntityAttributes(); } catch (err) { console.error('Delete attribute failed:', err); alert('删除失败'); } }; // Attribute History window.showAttributeHistory = async function(attributeName) { if (!currentEntityIdForAttributes) return; const modal = document.getElementById('attrHistoryModal'); modal.classList.add('show'); try { const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes/history?attribute_name=${encodeURIComponent(attributeName)}`); if (!res.ok) throw new Error('Failed to load history'); const data = await res.json(); renderAttributeHistory(data.history, attributeName); } catch (err) { console.error('Load history failed:', err); document.getElementById('attrHistoryContent').innerHTML = '

加载失败

'; } }; window.hideAttrHistoryModal = function() { document.getElementById('attrHistoryModal').classList.remove('show'); }; function renderAttributeHistory(history, attributeName) { const container = document.getElementById('attrHistoryContent'); if (history.length === 0) { container.innerHTML = `

属性 "${attributeName}" 暂无变更历史

`; return; } container.innerHTML = history.map(h => { const date = new Date(h.changed_at).toLocaleString(); return `
${h.changed_by || '系统'} ${date}
${h.old_value || '(无)'} ${h.new_value || '(无)'}
${h.change_reason ? `
原因: ${h.change_reason}
` : ''}
`; }).join(''); } // Attribute Templates Management window.showAttributeTemplates = async function() { const modal = document.getElementById('attrTemplatesModal'); modal.classList.add('show'); document.getElementById('templateForm').style.display = 'none'; document.getElementById('toggleTemplateBtn').style.display = 'inline-block'; document.getElementById('saveTemplateBtn').style.display = 'none'; await loadAttributeTemplates(); }; window.hideAttrTemplatesModal = function() { document.getElementById('attrTemplatesModal').classList.remove('show'); }; async function loadAttributeTemplates() { if (!currentProject) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/attribute-templates`); if (!res.ok) throw new Error('Failed to load templates'); currentTemplates = await res.json(); renderTemplatesList(); } catch (err) { console.error('Load templates failed:', err); document.getElementById('templatesList').innerHTML = '

加载失败

'; } } function renderTemplatesList() { const container = document.getElementById('templatesList'); if (currentTemplates.length === 0) { container.innerHTML = '

暂无模板,点击"新建模板"创建

'; return; } container.innerHTML = currentTemplates.map(t => { const optionsStr = t.options ? `选项: ${t.options.join(', ')}` : ''; return `
${t.name} ${t.type} ${t.is_required ? '*' : ''}
${t.description || ''} ${optionsStr}
`; }).join(''); } window.toggleTemplateForm = function() { const form = document.getElementById('templateForm'); const toggleBtn = document.getElementById('toggleTemplateBtn'); const saveBtn = document.getElementById('saveTemplateBtn'); if (form.style.display === 'none') { form.style.display = 'block'; toggleBtn.style.display = 'none'; saveBtn.style.display = 'inline-block'; } else { form.style.display = 'none'; toggleBtn.style.display = 'inline-block'; saveBtn.style.display = 'none'; } }; window.onTemplateTypeChange = function() { const type = document.getElementById('templateType').value; const optionsGroup = document.getElementById('templateOptionsGroup'); if (type === 'select' || type === 'multiselect') { optionsGroup.style.display = 'block'; } else { optionsGroup.style.display = 'none'; } }; window.saveTemplate = async function() { if (!currentProject) return; const name = document.getElementById('templateName').value.trim(); const type = document.getElementById('templateType').value; const description = document.getElementById('templateDesc').value.trim(); const isRequired = document.getElementById('templateRequired').checked; const defaultValue = document.getElementById('templateDefault').value.trim(); if (!name) { alert('请输入模板名称'); return; } let options = null; if (type === 'select' || type === 'multiselect') { const optionsStr = document.getElementById('templateOptions').value.trim(); if (optionsStr) { options = optionsStr.split(',').map(o => o.trim()).filter(o => o); } } try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/attribute-templates`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, type, description, options, is_required: isRequired, default_value: defaultValue || null }) }); if (!res.ok) throw new Error('Failed to save template'); // Reset form document.getElementById('templateName').value = ''; document.getElementById('templateDesc').value = ''; document.getElementById('templateOptions').value = ''; document.getElementById('templateDefault').value = ''; document.getElementById('templateRequired').checked = false; await loadAttributeTemplates(); toggleTemplateForm(); } catch (err) { console.error('Save template failed:', err); alert('保存失败'); } }; window.deleteTemplate = async function(templateId) { if (!confirm('确定要删除这个模板吗?')) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/attribute-templates/${templateId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete template'); await loadAttributeTemplates(); } catch (err) { console.error('Delete template failed:', err); alert('删除失败'); } }; // Search entities by attributes window.searchByAttributes = async function() { if (!currentProject) return; const filterName = document.getElementById('attrFilterName').value; const filterValue = document.getElementById('attrFilterValue').value; const filterOp = document.getElementById('attrFilterOp').value; if (!filterName || !filterValue) { alert('请输入筛选条件'); return; } try { const filters = JSON.stringify([{ name: filterName, value: filterValue, operator: filterOp }]); const res = await fetch(`${API_BASE}/projects/${currentProject.id}/entities/search-by-attributes?filters=${encodeURIComponent(filters)}`); if (!res.ok) throw new Error('Search failed'); const entities = await res.json(); // Update entity grid const grid = document.getElementById('kbEntityGrid'); if (entities.length === 0) { grid.innerHTML = '

未找到匹配的实体

'; return; } grid.innerHTML = entities.map(ent => `
${ent.type} ${ent.name}
${ent.definition || '暂无定义'}
`).join(''); } catch (err) { console.error('Search by attributes failed:', err); alert('搜索失败'); } };