Phase 3: Memory & Growth - Multi-file fusion, Entity alignment with embedding, Document import, Knowledge base panel

This commit is contained in:
OpenClaw Bot
2026-02-18 12:12:39 +08:00
parent 643fe46780
commit da8a4db985
11 changed files with 1842 additions and 167 deletions

View File

@@ -1,4 +1,5 @@
// InsightFlow Frontend - Phase 2 (Interactive Workbench)
// InsightFlow Frontend - Phase 3 (Memory & Growth)
// Knowledge Growth: Multi-file fusion + Entity Alignment + Document Import
const API_BASE = '/api/v1';
let currentProject = null;
@@ -7,8 +8,11 @@ let selectedEntity = null;
let projectRelations = [];
let projectEntities = [];
let currentTranscript = null;
let projectTranscripts = [];
let editMode = false;
let contextMenuTarget = null;
let currentUploadTab = 'audio';
let knowledgeBaseData = null;
// Init
document.addEventListener('DOMContentLoaded', () => {
@@ -70,6 +74,49 @@ async function uploadAudio(file) {
return await res.json();
}
// Phase 3: Document Upload API
async function uploadDocument(file) {
const formData = new FormData();
formData.append('file', file);
const res = await fetch(`${API_BASE}/projects/${currentProject.id}/upload-document`, {
method: 'POST',
body: formData
});
if (!res.ok) {
const error = await res.json();
throw new Error(error.detail || 'Document upload failed');
}
return await res.json();
}
// Phase 3: Knowledge Base API
async function fetchKnowledgeBase() {
const res = await fetch(`${API_BASE}/projects/${currentProject.id}/knowledge-base`);
if (!res.ok) throw new Error('Failed to fetch knowledge base');
return await res.json();
}
// Phase 3: Glossary API
async function addGlossaryTerm(term, pronunciation = '') {
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) throw new Error('Failed to add glossary term');
return await res.json();
}
async function deleteGlossaryTerm(termId) {
const res = await fetch(`${API_BASE}/glossary/${termId}`, {
method: 'DELETE'
});
if (!res.ok) throw new Error('Failed to delete glossary term');
return await res.json();
}
// Phase 2: Entity Edit API
async function updateEntity(entityId, data) {
const res = await fetch(`${API_BASE}/entities/${entityId}`, {
@@ -147,7 +194,7 @@ async function updateTranscript(transcriptId, fullText) {
async function loadProjectData() {
try {
// 并行加载实体关系
// 并行加载实体关系和转录列表
const [entitiesRes, relationsRes, transcriptsRes] = await Promise.all([
fetch(`${API_BASE}/projects/${currentProject.id}/entities`),
fetch(`${API_BASE}/projects/${currentProject.id}/relations`),
@@ -160,32 +207,173 @@ async function loadProjectData() {
if (relationsRes.ok) {
projectRelations = await relationsRes.json();
}
if (transcriptsRes.ok) {
projectTranscripts = await transcriptsRes.json();
}
// 加载最新的转录
if (transcriptsRes.ok) {
const transcripts = await transcriptsRes.json();
if (transcripts.length > 0) {
currentTranscript = await getTranscript(transcripts[0].id);
currentData = {
transcript_id: currentTranscript.id,
project_id: currentProject.id,
segments: [{ speaker: '全文', text: currentTranscript.full_text }],
entities: projectEntities,
full_text: currentTranscript.full_text,
created_at: currentTranscript.created_at
};
renderTranscript();
}
if (projectTranscripts.length > 0) {
currentTranscript = await getTranscript(projectTranscripts[0].id);
currentData = {
transcript_id: currentTranscript.id,
project_id: currentProject.id,
segments: [{ speaker: '全文', text: currentTranscript.full_text }],
entities: projectEntities,
full_text: currentTranscript.full_text,
created_at: currentTranscript.created_at
};
renderTranscript();
}
renderGraph();
renderEntityList();
renderTranscriptDropdown();
} catch (err) {
console.error('Load project data failed:', err);
}
}
// Phase 3: View Switching
window.switchView = function(viewName) {
// Update sidebar buttons
document.querySelectorAll('.sidebar-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
if (viewName === 'workbench') {
document.getElementById('workbenchView').style.display = 'flex';
document.getElementById('knowledgeBaseView').classList.remove('show');
} else if (viewName === 'knowledge-base') {
document.getElementById('workbenchView').style.display = 'none';
document.getElementById('knowledgeBaseView').classList.add('show');
loadKnowledgeBase();
}
};
// Phase 3: Load Knowledge Base
async function loadKnowledgeBase() {
try {
knowledgeBaseData = await fetchKnowledgeBase();
renderKnowledgeBase();
} catch (err) {
console.error('Load knowledge base failed:', err);
}
}
// Phase 3: Render Knowledge Base
function renderKnowledgeBase() {
if (!knowledgeBaseData) return;
// Update stats
document.getElementById('kbEntityCount').textContent = knowledgeBaseData.stats.entity_count;
document.getElementById('kbRelationCount').textContent = knowledgeBaseData.stats.relation_count;
document.getElementById('kbTranscriptCount').textContent = knowledgeBaseData.stats.transcript_count;
document.getElementById('kbGlossaryCount').textContent = knowledgeBaseData.stats.glossary_count;
// Render entities
const entityGrid = document.getElementById('kbEntityGrid');
entityGrid.innerHTML = knowledgeBaseData.entities.map(e => `
<div class="kb-entity-card" onclick="selectEntity('${e.id}'); switchView('workbench');">
<span class="entity-type-badge type-${e.type}">${e.type}</span>
<div class="kb-entity-name">${e.name}</div>
<div class="kb-entity-def">${e.definition || '暂无定义'}</div>
<div class="kb-entity-meta">提及 ${e.mention_count} 次 | 出现在 ${e.appears_in.length} 个文件中</div>
</div>
`).join('');
// Render relations
const relationsList = document.getElementById('kbRelationsList');
relationsList.innerHTML = knowledgeBaseData.relations.map(r => `
<div class="kb-glossary-item">
<div>
<strong>${r.source_name}</strong>
<span style="color:#666;">→ ${r.type} →</span>
<strong>${r.target_name}</strong>
<div style="font-size:0.8rem;color:#666;margin-top:4px;">${r.evidence || '无证据'}</div>
</div>
</div>
`).join('');
// Render glossary
const glossaryList = document.getElementById('kbGlossaryList');
glossaryList.innerHTML = knowledgeBaseData.glossary.map(g => `
<div class="kb-glossary-item">
<div>
<strong>${g.term}</strong>
${g.pronunciation ? `<span style="color:#666;font-size:0.85rem;"> (${g.pronunciation})</span>` : ''}
<span style="color:#00d4ff;font-size:0.8rem;margin-left:8px;">出现 ${g.frequency} 次</span>
</div>
<button class="btn-icon" onclick="deleteGlossaryTerm('${g.id}').then(loadKnowledgeBase)">删除</button>
</div>
`).join('');
// Render transcripts
const transcriptsList = document.getElementById('kbTranscriptsList');
transcriptsList.innerHTML = knowledgeBaseData.transcripts.map(t => `
<div class="kb-transcript-item">
<div>
<span class="file-type-icon type-${t.type}">${t.type === 'audio' ? '🎵' : '📄'}</span>
<span style="margin-left:8px;">${t.filename}</span>
</div>
<span style="color:#666;font-size:0.8rem;">${new Date(t.created_at).toLocaleDateString()}</span>
</div>
`).join('');
}
// Phase 3: KB Tab Switching
window.switchKBTab = function(tabName) {
document.querySelectorAll('.kb-nav-item').forEach(item => {
item.classList.remove('active');
});
event.target.classList.add('active');
document.querySelectorAll('.kb-section').forEach(section => {
section.classList.remove('active');
});
document.getElementById(`kb${tabName.charAt(0).toUpperCase() + tabName.slice(1)}Section`).classList.add('active');
};
// Phase 3: Transcript Dropdown
window.toggleTranscriptDropdown = function() {
const dropdown = document.getElementById('transcriptDropdown');
dropdown.classList.toggle('show');
};
function renderTranscriptDropdown() {
const dropdown = document.getElementById('transcriptDropdown');
if (!dropdown || projectTranscripts.length === 0) return;
dropdown.innerHTML = projectTranscripts.map(t => `
<div class="transcript-option ${currentTranscript && currentTranscript.id === t.id ? 'active' : ''}"
onclick="switchTranscript('${t.id}')">
<span class="file-type-icon type-${t.type || 'audio'}">${(t.type || 'audio') === 'audio' ? '🎵' : '📄'}</span>
<span style="margin-left:4px;">${t.filename}</span>
</div>
`).join('');
}
window.switchTranscript = async function(transcriptId) {
try {
currentTranscript = await getTranscript(transcriptId);
currentData = {
transcript_id: currentTranscript.id,
project_id: currentProject.id,
segments: [{ speaker: '全文', text: currentTranscript.full_text }],
entities: projectEntities,
full_text: currentTranscript.full_text,
created_at: currentTranscript.created_at
};
renderTranscript();
renderTranscriptDropdown();
document.getElementById('transcriptDropdown').classList.remove('show');
} catch (err) {
console.error('Switch transcript failed:', err);
alert('切换文件失败');
}
};
// Phase 2: Transcript Edit Mode
window.toggleEditMode = function() {
editMode = !editMode;
@@ -255,7 +443,7 @@ function renderTranscript() {
const div = document.createElement('div');
div.className = 'segment';
div.innerHTML = `
<div class="speaker">转录文本</div>
<div class="speaker">${currentTranscript.filename || '转录文本'}</div>
<div class="segment-text">${text}</div>
`;
@@ -311,7 +499,7 @@ function renderGraph() {
.attr('y', '50%')
.attr('text-anchor', 'middle')
.attr('fill', '#666')
.text('暂无实体数据,请上传音频');
.text('暂无实体数据,请上传音频或文档');
return;
}
@@ -458,7 +646,7 @@ function renderEntityList() {
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>';
container.innerHTML += '<p style="color:#666;font-size:0.85rem;">暂无实体,请上传音频或文档文件</p>';
return;
}
@@ -788,6 +976,28 @@ window.createEntityFromSelection = async function() {
}
};
// Phase 3: Upload Tab Switching
window.switchUploadTab = function(tab) {
currentUploadTab = tab;
document.querySelectorAll('.upload-tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
const hint = document.getElementById('uploadHint');
if (tab === 'audio') {
hint.textContent = '支持 MP3, WAV, M4A (最大 500MB)';
} else {
hint.textContent = '支持 PDF, DOCX, DOC, TXT, MD';
}
};
window.triggerFileSelect = function() {
if (currentUploadTab === 'audio') {
document.getElementById('fileInput').click();
} else {
document.getElementById('docInput').click();
}
};
// Show/hide upload
window.showUpload = function() {
const el = document.getElementById('uploadOverlay');
@@ -799,49 +1009,120 @@ window.hideUpload = function() {
if (el) el.classList.remove('show');
};
// Phase 3: Glossary Modal
window.showAddTermModal = function() {
document.getElementById('glossaryModal').classList.add('show');
};
window.hideGlossaryModal = function() {
document.getElementById('glossaryModal').classList.remove('show');
document.getElementById('glossaryTerm').value = '';
document.getElementById('glossaryPronunciation').value = '';
};
window.saveGlossaryTerm = async function() {
const term = document.getElementById('glossaryTerm').value.trim();
const pronunciation = document.getElementById('glossaryPronunciation').value.trim();
if (!term) {
alert('请输入术语');
return;
}
try {
await addGlossaryTerm(term, pronunciation);
hideGlossaryModal();
loadKnowledgeBase();
} catch (err) {
console.error('Add term failed:', err);
alert('添加术语失败: ' + err.message);
}
};
// Upload handling
function initUpload() {
const input = document.getElementById('fileInput');
// Audio upload
const audioInput = document.getElementById('fileInput');
if (audioInput) {
audioInput.addEventListener('change', async (e) => {
if (!e.target.files.length) return;
await handleFileUpload(e.target.files[0], 'audio');
});
}
// Document upload
const docInput = document.getElementById('docInput');
if (docInput) {
docInput.addEventListener('change', async (e) => {
if (!e.target.files.length) return;
await handleFileUpload(e.target.files[0], 'document');
});
}
}
async function handleFileUpload(file, type) {
const overlay = document.getElementById('uploadOverlay');
if (!input) return;
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;">${type === 'audio' ? 'ASR转录 + 实体提取中' : '文档解析 + 实体提取中'}</p>
</div>
`;
input.addEventListener('change', async (e) => {
if (!e.target.files.length) return;
try {
let result;
if (type === 'audio') {
result = await uploadAudio(file);
} else {
result = await uploadDocument(file);
}
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>
// 更新当前数据
currentData = result;
// 重新加载项目数据
await loadProjectData();
// 重置上传界面
overlay.innerHTML = `
<div class="upload-box">
<h2 style="margin-bottom:10px;">上传文件</h2>
<div class="upload-tabs">
<div class="upload-tab active" onclick="switchUploadTab('audio')">🎵 音频</div>
<div class="upload-tab" onclick="switchUploadTab('document')">📄 文档</div>
</div>
`;
}
<p style="color:#666;" id="uploadHint">支持 MP3, WAV, M4A (最大 500MB)</p>
<input type="file" id="fileInput" accept="audio/*" hidden>
<input type="file" id="docInput" accept=".pdf,.docx,.doc,.txt,.md" hidden>
<button class="btn" onclick="triggerFileSelect()">选择文件</button>
<br><br>
<button class="btn btn-secondary" onclick="hideUpload()">取消</button>
</div>
`;
try {
const result = await uploadAudio(file);
// 更新当前数据
currentData = result;
// 重新加载项目数据
await loadProjectData();
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>
`;
}
}
});
// 重新绑定事件
initUpload();
overlay.classList.remove('show');
} catch (err) {
console.error('Upload failed:', err);
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>
`;
}
}
// Close dropdown when clicking outside
document.addEventListener('click', (e) => {
const dropdown = document.getElementById('transcriptDropdown');
const selector = document.querySelector('.transcript-selector');
if (dropdown && selector && !selector.contains(e.target)) {
dropdown.classList.remove('show');
}
});

View File

@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>InsightFlow - 知识工作台 (Phase 2)</title>
<title>InsightFlow - 知识工作台 (Phase 3)</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
@@ -46,10 +46,44 @@
color: #888;
font-size: 0.9rem;
}
.header-actions {
display: flex;
gap: 10px;
}
.main {
display: flex;
height: calc(100vh - 50px);
}
.sidebar {
width: 60px;
background: #111;
border-right: 1px solid #222;
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0;
}
.sidebar-btn {
width: 44px;
height: 44px;
background: transparent;
border: none;
color: #666;
font-size: 1.2rem;
cursor: pointer;
border-radius: 8px;
margin-bottom: 8px;
transition: all 0.2s;
}
.sidebar-btn:hover, .sidebar-btn.active {
background: #1a1a1a;
color: #00d4ff;
}
.content-area {
flex: 1;
display: flex;
overflow: hidden;
}
.editor-panel {
width: 50%;
border-right: 1px solid #222;
@@ -199,10 +233,29 @@
border-radius: 16px;
padding: 60px;
text-align: center;
max-width: 500px;
}
.upload-box:hover {
border-color: #00d4ff;
}
.upload-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
justify-content: center;
}
.upload-tab {
padding: 8px 16px;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 6px;
cursor: pointer;
color: #888;
}
.upload-tab.active {
border-color: #00d4ff;
color: #00d4ff;
}
.btn {
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
color: white;
@@ -373,6 +426,164 @@
.node-label {
pointer-events: none;
}
/* Phase 3: Knowledge Base Panel */
.kb-panel {
width: 100%;
height: 100%;
display: none;
flex-direction: column;
background: #0a0a0a;
}
.kb-panel.show {
display: flex;
}
.kb-header {
padding: 16px 20px;
background: #141414;
border-bottom: 1px solid #222;
display: flex;
justify-content: space-between;
align-items: center;
}
.kb-stats {
display: flex;
gap: 24px;
}
.kb-stat {
text-align: center;
}
.kb-stat-value {
font-size: 1.5rem;
font-weight: 600;
color: #00d4ff;
}
.kb-stat-label {
font-size: 0.75rem;
color: #666;
}
.kb-content {
flex: 1;
display: flex;
overflow: hidden;
}
.kb-sidebar {
width: 200px;
background: #111;
border-right: 1px solid #222;
padding: 16px 0;
}
.kb-nav-item {
padding: 12px 20px;
cursor: pointer;
color: #888;
border-left: 3px solid transparent;
}
.kb-nav-item:hover {
background: #1a1a1a;
color: #e0e0e0;
}
.kb-nav-item.active {
background: #1a1a1a;
color: #00d4ff;
border-left-color: #00d4ff;
}
.kb-main {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.kb-section {
display: none;
}
.kb-section.active {
display: block;
}
.kb-entity-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
}
.kb-entity-card {
background: #141414;
border: 1px solid #222;
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.2s;
}
.kb-entity-card:hover {
border-color: #00d4ff;
}
.kb-entity-name {
font-weight: 600;
margin-bottom: 4px;
}
.kb-entity-def {
font-size: 0.85rem;
color: #888;
margin-bottom: 8px;
}
.kb-entity-meta {
font-size: 0.75rem;
color: #666;
}
.kb-glossary-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background: #141414;
border-radius: 6px;
margin-bottom: 8px;
}
.kb-transcript-item {
padding: 12px 16px;
background: #141414;
border-radius: 6px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-type-icon {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
}
.type-audio { background: #7b2cbf; }
.type-document { background: #00d4ff; color: #000; }
/* Transcript selector */
.transcript-selector {
position: relative;
}
.transcript-dropdown {
position: absolute;
top: 100%;
right: 0;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
min-width: 200px;
max-height: 300px;
overflow-y: auto;
display: none;
z-index: 100;
}
.transcript-dropdown.show {
display: block;
}
.transcript-option {
padding: 10px 16px;
cursor: pointer;
border-bottom: 1px solid #222;
}
.transcript-option:hover {
background: #2a2a2a;
}
.transcript-option.active {
background: #00d4ff22;
}
</style>
</head>
<body>
@@ -381,35 +592,113 @@
<a href="/" class="back-link">← 返回项目列表</a>
<span class="project-name" id="projectName">加载中...</span>
</div>
<button class="btn btn-small" onclick="showUpload()">+ 上传音频</button>
<div class="header-actions">
<button class="btn btn-small" onclick="showUpload()">+ 上传文件</button>
</div>
</div>
<div class="main">
<div class="editor-panel">
<div class="panel-header">
<span>📄 转录文本</span>
<div class="panel-actions">
<button class="btn-icon" onclick="toggleEditMode()" id="editBtn">✏️ 编辑</button>
<button class="btn-icon" onclick="saveTranscript()" id="saveBtn" style="display:none;">💾 保存</button>
</div>
</div>
<div class="transcript-content" id="transcriptContent">
<div class="empty-state">
<p style="color:#666;">暂无转录内容</p>
<button class="btn" onclick="showUpload()">上传音频</button>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<button class="sidebar-btn active" onclick="switchView('workbench')" title="工作台">📝</button>
<button class="sidebar-btn" onclick="switchView('knowledge-base')" title="知识库">📚</button>
</div>
<div class="graph-panel">
<div class="panel-header">
<span>🔗 知识图谱</span>
<span style="font-size:0.8rem;color:#666;">右键节点编辑 | 拖拽建立关系</span>
<!-- Content Area -->
<div class="content-area">
<!-- Workbench View -->
<div id="workbenchView" class="workbench-view" style="display: flex; width: 100%;">
<div class="editor-panel">
<div class="panel-header">
<div style="display: flex; align-items: center; gap: 12px;">
<span>📄 转录文本</span>
<div class="transcript-selector">
<button class="btn-icon" onclick="toggleTranscriptDropdown()">📁 选择文件</button>
<div class="transcript-dropdown" id="transcriptDropdown"></div>
</div>
</div>
<div class="panel-actions">
<button class="btn-icon" onclick="toggleEditMode()" id="editBtn">✏️ 编辑</button>
<button class="btn-icon" onclick="saveTranscript()" id="saveBtn" style="display:none;">💾 保存</button>
</div>
</div>
<div class="transcript-content" id="transcriptContent">
<div class="empty-state">
<p style="color:#666;">暂无转录内容</p>
<button class="btn" onclick="showUpload()">上传音频或文档</button>
</div>
</div>
</div>
<div class="graph-panel">
<div class="panel-header">
<span>🔗 知识图谱</span>
<span style="font-size:0.8rem;color:#666;">右键节点编辑 | 拖拽建立关系</span>
</div>
<svg id="graph-svg"></svg>
<div class="entity-list" id="entityList">
<h3 style="margin-bottom:12px;color:#888;font-size:0.9rem;">项目实体</h3>
<p style="color:#666;font-size:0.85rem;">暂无实体数据</p>
</div>
</div>
</div>
<svg id="graph-svg"></svg>
<div class="entity-list" id="entityList">
<h3 style="margin-bottom:12px;color:#888;font-size:0.9rem;">项目实体</h3>
<p style="color:#666;font-size:0.85rem;">暂无实体数据</p>
<!-- Knowledge Base View -->
<div id="knowledgeBaseView" class="kb-panel">
<div class="kb-header">
<h2>📚 项目知识库</h2>
<div class="kb-stats">
<div class="kb-stat">
<div class="kb-stat-value" id="kbEntityCount">0</div>
<div class="kb-stat-label">实体</div>
</div>
<div class="kb-stat">
<div class="kb-stat-value" id="kbRelationCount">0</div>
<div class="kb-stat-label">关系</div>
</div>
<div class="kb-stat">
<div class="kb-stat-value" id="kbTranscriptCount">0</div>
<div class="kb-stat-label">文件</div>
</div>
<div class="kb-stat">
<div class="kb-stat-value" id="kbGlossaryCount">0</div>
<div class="kb-stat-label">术语</div>
</div>
</div>
</div>
<div class="kb-content">
<div class="kb-sidebar">
<div class="kb-nav-item active" onclick="switchKBTab('entities')">🏷️ 实体</div>
<div class="kb-nav-item" onclick="switchKBTab('relations')">🔗 关系</div>
<div class="kb-nav-item" onclick="switchKBTab('glossary')">📖 术语表</div>
<div class="kb-nav-item" onclick="switchKBTab('transcripts')">📁 文件</div>
</div>
<div class="kb-main">
<!-- Entities Section -->
<div class="kb-section active" id="kbEntitiesSection">
<h3 style="margin-bottom:16px;">所有实体</h3>
<div class="kb-entity-grid" id="kbEntityGrid"></div>
</div>
<!-- Relations Section -->
<div class="kb-section" id="kbRelationsSection">
<h3 style="margin-bottom:16px;">所有关系</h3>
<div id="kbRelationsList"></div>
</div>
<!-- Glossary Section -->
<div class="kb-section" id="kbGlossarySection">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;">
<h3>术语表</h3>
<button class="btn btn-small" onclick="showAddTermModal()">+ 添加术语</button>
</div>
<div id="kbGlossaryList"></div>
</div>
<!-- Transcripts Section -->
<div class="kb-section" id="kbTranscriptsSection">
<h3 style="margin-bottom:16px;">所有文件</h3>
<div id="kbTranscriptsList"></div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -417,10 +706,15 @@
<!-- Upload Modal -->
<div class="upload-overlay" id="uploadOverlay">
<div class="upload-box">
<h2 style="margin-bottom:10px;">上传音频分析</h2>
<p style="color:#666;">支持 MP3, WAV, M4A (最大 500MB)</p>
<h2 style="margin-bottom:10px;">上传文件</h2>
<div class="upload-tabs">
<div class="upload-tab active" onclick="switchUploadTab('audio')">🎵 音频</div>
<div class="upload-tab" onclick="switchUploadTab('document')">📄 文档</div>
</div>
<p style="color:#666;" id="uploadHint">支持 MP3, WAV, M4A (最大 500MB)</p>
<input type="file" id="fileInput" accept="audio/*" hidden>
<button class="btn" onclick="document.getElementById('fileInput').click()">选择文件</button>
<input type="file" id="docInput" accept=".pdf,.docx,.doc,.txt,.md" hidden>
<button class="btn" onclick="triggerFileSelect()">选择文件</button>
<br><br>
<button class="btn btn-secondary" onclick="hideUpload()">取消</button>
</div>
@@ -514,6 +808,25 @@
</div>
</div>
<!-- Add Glossary Term Modal -->
<div class="modal-overlay" id="glossaryModal">
<div class="modal">
<h3 class="modal-header">添加术语</h3>
<div class="form-group">
<label>术语</label>
<input type="text" id="glossaryTerm" placeholder="术语名称">
</div>
<div class="form-group">
<label>发音提示 (可选)</label>
<input type="text" id="glossaryPronunciation" placeholder="如: K8s 发音为 Kubernetes">
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="hideGlossaryModal()">取消</button>
<button class="btn" onclick="saveGlossaryTerm()">添加</button>
</div>
</div>
</div>
<!-- Context Menu -->
<div class="context-menu" id="contextMenu">
<div class="context-menu-item" onclick="editEntity()">✏️ 编辑实体</div>