Files
insightflow/frontend/app.js
OpenClaw Bot 9dd54b3a38 Phase 5: 知识推理与问答增强
- 新增 knowledge_reasoner.py 推理引擎
- 支持因果/对比/时序/关联四种推理类型
- 智能项目总结 API (全面/高管/技术/风险)
- 实体关联路径发现功能
- 前端推理面板 UI 和交互
- 更新 API 端点和健康检查

Refs: Phase 5 开发任务
2026-02-19 18:07:00 +08:00

1378 lines
47 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.
// 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 = `
<div class="message-bubble typing-indicator" id="typingIndicator">
<span></span><span></span><span></span>
</div>
`;
} else {
msgDiv.innerHTML = `<div class="message-bubble">${content}</div>`;
}
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 = '<div style="margin-bottom:8px;">💡 基于项目数据的建议:</div>';
suggestions.forEach((s, i) => {
html += `
<div class="suggestion-card" onclick="applySuggestion(${i})">
<div class="suggestion-type">${s.type === 'action' ? '⚡ 操作' : '💡 洞察'}</div>
<div class="suggestion-title">${s.title}</div>
<div class="suggestion-desc">${s.description}</div>
</div>
`;
});
const msgDiv = document.createElement('div');
msgDiv.className = 'chat-message assistant';
msgDiv.innerHTML = `<div class="message-bubble" style="padding:0;background:none;border:none;">${html}</div>`;
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 + `<span class="entity ${lowConfClass}" data-id="${ent.id}" onclick="window.selectEntity('${ent.id}')" onmouseenter="showEntityCard(event, '${ent.id}')" onmouseleave="hideEntityCard()">${name}</span>` + after;
});
div.innerHTML = `
<div class="speaker">${seg.speaker}</div>
<div class="segment-text">${text}</div>
`;
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 = '<p style="color:#666;">加载中...</p>';
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 = `
<div style="margin-bottom:20px;">
<div style="color:#888;font-size:0.85rem;margin-bottom:8px;">关系类型</div>
<div style="font-size:1.1rem;color:#00d4ff;">${data.source}${data.type}${data.target}</div>
</div>
<div style="margin-bottom:20px;">
<div style="color:#888;font-size:0.85rem;margin-bottom:8px;">来源文档</div>
<div>${data.transcript?.filename || '未知文件'}</div>
</div>
<div style="color:#888;font-size:0.85rem;margin-bottom:8px;">证据文本</div>
<div class="provenance-evidence">"${data.evidence || '无证据文本'}"</div>
`;
} else {
content = '<p style="color:#ff6b6b;">获取溯源信息失败</p>';
}
} else {
// 使用本地数据
content = `
<div style="margin-bottom:20px;">
<div style="color:#888;font-size:0.85rem;margin-bottom:8px;">关系类型</div>
<div style="font-size:1.1rem;color:#00d4ff;">${relation.source.name || relation.source}${relation.type}${relation.target.name || relation.target}</div>
</div>
<div style="color:#888;font-size:0.85rem;margin-bottom:8px;">证据文本</div>
<div class="provenance-evidence">"${relation.evidence || '无证据文本'}"</div>
`;
}
body.innerHTML = content;
} catch (err) {
body.innerHTML = '<p style="color:#ff6b6b;">加载失败</p>';
}
}
function closeProvenance() {
document.getElementById('provenanceModal').classList.remove('show');
}
// ==================== Entity List ====================
function renderEntityList() {
const container = document.getElementById('entityList');
if (!container) return;
container.innerHTML = '<h3 style="margin-bottom:12px;color:#888;font-size:0.9rem;">项目实体</h3>';
if (!projectEntities || projectEntities.length === 0) {
container.innerHTML += '<p style="color:#666;font-size:0.85rem;">暂无实体,请上传音频文件</p>';
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 = `
<span class="entity-type-badge type-${ent.type.toLowerCase()}">${ent.type}</span>
<div>
<div style="font-weight:500;">${ent.name}</div>
<div style="font-size:0.8rem;color:#666;">${ent.definition || '暂无定义'}</div>
</div>
`;
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 = `
<div style="text-align:center;">
<h2>正在分析...</h2>
<p style="color:#666;margin-top:10px;">${file.name}</p>
<p style="color:#888;margin-top:20px;font-size:0.9rem;">ASR转录 + 实体提取中</p>
</div>
`;
}
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 = `
<div style="text-align:center;">
<h2 style="color:#ff6b6b;">分析失败</h2>
<p style="color:#666;margin-top:10px;">${err.message}</p>
<button class="btn" onclick="location.reload()" style="margin-top:20px;">重试</button>
</div>
`;
}
}
});
}
// ==================== Phase 5: Timeline View ====================
async function loadTimeline() {
const container = document.getElementById('timelineContainer');
const entityFilter = document.getElementById('timelineEntityFilter');
if (!container) return;
container.innerHTML = '<div class="timeline-empty"><p>加载时间线数据...</p></div>';
try {
// 更新实体筛选器选项
if (entityFilter && projectEntities.length > 0) {
const currentValue = entityFilter.value;
entityFilter.innerHTML = '<option value="">全部实体</option>';
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 = '<div class="timeline-empty"><p style="color:#ff6b6b;">加载失败,请重试</p></div>';
}
}
function renderTimeline(events) {
const container = document.getElementById('timelineContainer');
if (events.length === 0) {
container.innerHTML = `
<div class="timeline-empty">
<p>暂无时间线数据</p>
<p style="font-size:0.85rem;margin-top:8px;">请先上传音频或文档文件</p>
</div>
`;
return;
}
// 按日期分组
const grouped = groupEventsByDate(events);
let html = '<div class="timeline-line"></div>';
Object.entries(grouped).forEach(([date, dayEvents]) => {
const dateLabel = formatDateLabel(date);
html += `
<div class="timeline-date-group">
<div class="timeline-date-header">
<div class="timeline-date-label">${dateLabel}</div>
<div class="timeline-date-dot"></div>
</div>
<div class="timeline-events">
`;
dayEvents.forEach(event => {
html += renderTimelineEvent(event);
});
html += '</div></div>';
});
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 `
<div class="timeline-event" onclick="selectEntity('${event.entity_id}')">
<div class="timeline-event-header">
<span class="timeline-event-type mention">提及</span>
<span class="timeline-event-entity">${event.entity_name}</span>
<span class="entity-type-badge type-${event.entity_type?.toLowerCase() || 'other'}" style="font-size:0.65rem;padding:1px 6px;">${event.entity_type || 'OTHER'}</span>
</div>
<div class="timeline-event-snippet">"${event.text_snippet || ''}"</div>
<div class="timeline-event-source">
<span>📄 ${event.source?.filename || '未知文件'}</span>
${event.confidence ? `<span>置信度: ${(event.confidence * 100).toFixed(0)}%</span>` : ''}
</div>
</div>
`;
} else if (event.type === 'relation') {
return `
<div class="timeline-event">
<div class="timeline-event-header">
<span class="timeline-event-type relation">关系</span>
<span class="timeline-event-entity">${event.source_entity}${event.target_entity}</span>
</div>
<div style="color:#888;font-size:0.85rem;margin-bottom:8px;">
关系类型: <span style="color:#00d4ff;">${event.relation_type}</span>
</div>
${event.evidence ? `<div class="timeline-event-snippet">"${event.evidence}"</div>` : ''}
<div class="timeline-event-source">
<span>📄 ${event.source?.filename || '未知文件'}</span>
</div>
</div>
`;
}
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);
};
card.innerHTML = `
<div class="kb-entity-name">
<span class="entity-type-badge type-${ent.type.toLowerCase()}" style="font-size:0.65rem;margin-right:8px;">${ent.type}</span>
${ent.name}
</div>
<div class="kb-entity-def">${ent.definition || '暂无定义'}</div>
<div class="kb-entity-meta">
📍 ${ent.mention_count || 0} 次提及 ·
${ent.appears_in?.length || 0} 个文件
</div>
`;
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 = `
<div>
<strong>${rel.source_name}</strong>
<span style="color:#00d4ff;">→ ${rel.type} →</span>
<strong>${rel.target_name}</strong>
${rel.evidence ? `<div style="color:#666;font-size:0.8rem;margin-top:4px;">"${rel.evidence.substring(0, 100)}..."</div>` : ''}
</div>
`;
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 = `
<div>
<strong>${term.term}</strong>
${term.pronunciation ? `<span style="color:#666;font-size:0.85rem;margin-left:8px;">(${term.pronunciation})</span>` : ''}
<span style="color:#00d4ff;font-size:0.8rem;margin-left:8px;">出现 ${term.frequency} 次</span>
</div>
<button class="btn-icon" onclick="deleteGlossaryTerm('${term.id}')">删除</button>
`;
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 = `
<div>
<span class="file-type-icon type-${t.type}">${t.type === 'audio' ? '🎵' : '📄'}</span>
<span style="margin-left:8px;">${t.filename}</span>
<div style="color:#666;font-size:0.8rem;margin-top:4px;">${new Date(t.created_at).toLocaleString()}</div>
</div>
`;
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 = `
<div class="reasoning-result">
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
<p style="color:#666;text-align:center;">正在进行知识推理...</p>
</div>
`;
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 = `
<div class="reasoning-result">
<p style="color:#ff6b6b;">推理失败,请稍后重试</p>
</div>
`;
}
};
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 = `
<div class="reasoning-evidence">
<h4>📋 支撑证据</h4>
${data.evidence.map(e => `<div class="evidence-item">${e.text || e}</div>`).join('')}
</div>
`;
}
let gapsHtml = '';
if (data.knowledge_gaps && data.knowledge_gaps.length > 0) {
gapsHtml = `
<div class="reasoning-gaps">
<h4>⚠️ 知识缺口</h4>
<ul>
${data.knowledge_gaps.map(g => `<li>${g}</li>`).join('')}
</ul>
</div>
`;
}
resultsDiv.innerHTML = `
<div class="reasoning-result">
<div class="reasoning-result-header">
<div class="reasoning-result-type">
<span>${typeLabel}</span>
</div>
<div class="reasoning-confidence">
置信度: ${confidencePercent}%
</div>
</div>
<div class="reasoning-answer">
${data.answer.replace(/\n/g, '<br>')}
</div>
${evidenceHtml}
${gapsHtml}
</div>
`;
}
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 = `
<div class="reasoning-result">
<div class="typing-indicator">
<span></span><span></span><span></span>
</div>
<p style="color:#666;text-align:center;">正在生成项目总结...</p>
</div>
`;
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 = `
<div class="reasoning-result">
<p style="color:#ff6b6b;">总结生成失败,请稍后重试</p>
</div>
`;
}
};
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 = `
<div style="margin: 16px 0;">
<h4 style="color:#888;font-size:0.9rem;margin-bottom:12px;">📌 关键要点</h4>
<ul style="margin:0;padding-left:20px;color:#aaa;line-height:1.8;">
${data.key_points.map(p => `<li>${p}</li>`).join('')}
</ul>
</div>
`;
}
let risksHtml = '';
if (data.risks && data.risks.length > 0) {
risksHtml = `
<div class="reasoning-gaps">
<h4>⚠️ 风险与问题</h4>
<ul>
${data.risks.map(r => `<li>${r}</li>`).join('')}
</ul>
</div>
`;
}
let recommendationsHtml = '';
if (data.recommendations && data.recommendations.length > 0) {
recommendationsHtml = `
<div class="reasoning-evidence">
<h4>💡 建议</h4>
${data.recommendations.map(r => `<div class="evidence-item">${r}</div>`).join('')}
</div>
`;
}
resultsDiv.innerHTML = `
<div class="reasoning-result">
<div class="reasoning-result-header">
<div class="reasoning-result-type">
<span>${typeLabel}</span>
</div>
<div class="reasoning-confidence">
置信度: ${Math.round(data.confidence * 100)}%
</div>
</div>
<div class="reasoning-answer">
${data.overview ? data.overview.replace(/\n/g, '<br>') : ''}
</div>
${keyPointsHtml}
${risksHtml}
${recommendationsHtml}
</div>
`;
}
window.findInferencePath = async function(startEntity, endEntity) {
const pathsSection = document.getElementById('inferencePathsSection');
const pathsList = document.getElementById('inferencePathsList');
pathsSection.style.display = 'block';
pathsList.innerHTML = '<p style="color:#666;">正在搜索关联路径...</p>';
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 = '<p style="color:#ff6b6b;">路径搜索失败</p>';
}
};
function renderInferencePaths(data) {
const pathsList = document.getElementById('inferencePathsList');
if (!data.paths || data.paths.length === 0) {
pathsList.innerHTML = '<p style="color:#666;">未找到关联路径</p>';
return;
}
pathsList.innerHTML = data.paths.map((path, idx) => {
const strengthPercent = Math.round(path.strength * 100);
const pathHtml = path.path.map((node, i) => {
if (i === 0) {
return `<span class="path-node">${node.entity}</span>`;
}
return `
<span class="path-arrow">→ ${node.relation} →</span>
<span class="path-node">${node.entity}</span>
`;
}).join('');
return `
<div class="inference-path">
<div class="inference-path-header">
<span style="color:#888;font-size:0.85rem;">路径 ${idx + 1}</span>
<span class="path-strength">关联强度: ${strengthPercent}%</span>
</div>
<div class="path-visual">
${pathHtml}
</div>
</div>
`;
}).join('');
}