Phase 3: Memory & Growth - Multi-file fusion, Entity alignment with embedding, Document import, Knowledge base panel
This commit is contained in:
393
frontend/app.js
393
frontend/app.js
@@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user