Files
insightflow/frontend/workbench.html

848 lines
28 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>InsightFlow - 知识工作台 (Phase 3)</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0a0a0a;
color: #e0e0e0;
height: 100vh;
overflow: hidden;
}
.header {
height: 50px;
background: #111;
border-bottom: 1px solid #222;
display: flex;
align-items: center;
padding: 0 20px;
justify-content: space-between;
}
.header h1 {
font-size: 1.2rem;
background: linear-gradient(90deg, #00d4ff, #7b2cbf);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.header-left {
display: flex;
align-items: center;
gap: 20px;
}
.back-link {
color: #666;
text-decoration: none;
font-size: 0.9rem;
}
.back-link:hover {
color: #00d4ff;
}
.project-name {
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;
display: flex;
flex-direction: column;
}
.panel-header {
padding: 12px 20px;
background: #141414;
border-bottom: 1px solid #222;
font-size: 0.9rem;
color: #888;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-actions {
display: flex;
gap: 8px;
}
.btn-icon {
background: transparent;
border: 1px solid #333;
color: #888;
padding: 4px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8rem;
}
.btn-icon:hover {
border-color: #00d4ff;
color: #00d4ff;
}
.transcript-content {
flex: 1;
padding: 20px;
overflow-y: auto;
line-height: 1.8;
font-size: 1rem;
}
.segment {
margin-bottom: 16px;
padding: 12px;
background: #141414;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s;
}
.segment:hover {
background: #1a1a1a;
}
.speaker {
color: #00d4ff;
font-weight: 600;
font-size: 0.85rem;
margin-bottom: 4px;
}
.segment-text {
color: #e0e0e0;
outline: none;
}
.segment-text[contenteditable="true"] {
background: #1a1a1a;
padding: 8px;
border-radius: 4px;
border: 1px solid #00d4ff;
}
.entity {
background: rgba(123, 44, 191, 0.3);
border-bottom: 2px solid #7b2cbf;
padding: 0 4px;
border-radius: 3px;
cursor: pointer;
position: relative;
}
.entity:hover {
background: rgba(123, 44, 191, 0.5);
}
.entity.selected {
background: #ff6b6b;
border-color: #ff6b6b;
color: #fff;
}
.graph-panel {
width: 50%;
display: flex;
flex-direction: column;
}
#graph-svg {
flex: 1;
background: #0a0a0a;
}
.entity-list {
height: 200px;
border-top: 1px solid #222;
background: #111;
padding: 16px;
overflow-y: auto;
}
.entity-item {
display: flex;
align-items: center;
padding: 8px 12px;
background: #1a1a1a;
border-radius: 6px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
}
.entity-item:hover {
background: #222;
}
.entity-item.selected {
background: #2a2a2a;
border-left: 3px solid #ff6b6b;
}
.entity-type-badge {
padding: 2px 8px;
border-radius: 4px;
font-size: 0.7rem;
font-weight: 600;
margin-right: 12px;
text-transform: uppercase;
}
.type-PROJECT { background: #7b2cbf; }
.type-TECH { background: #00d4ff; color: #000; }
.type-PERSON { background: #ff6b6b; }
.type-ORG { background: #4ecdc4; color: #000; }
.type-OTHER { background: #666; }
.upload-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.95);
display: none;
align-items: center;
justify-content: center;
z-index: 2000;
}
.upload-overlay.show {
display: flex;
}
.upload-box {
border: 2px dashed #333;
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;
border: none;
padding: 12px 32px;
border-radius: 8px;
font-size: 1rem;
cursor: pointer;
margin-top: 20px;
}
.btn:hover {
opacity: 0.9;
}
.btn-small {
padding: 8px 16px;
font-size: 0.85rem;
margin-top: 0;
}
.btn-danger {
background: #ff6b6b;
}
.btn-secondary {
background: #333;
}
.empty-state {
text-align: center;
padding: 60px 20px;
}
/* Phase 2: Entity Editor Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 3000;
}
.modal-overlay.show {
display: flex;
}
.modal {
background: #1a1a1a;
border-radius: 12px;
padding: 24px;
width: 90%;
max-width: 500px;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
font-size: 1.2rem;
margin-bottom: 20px;
color: #fff;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
color: #888;
font-size: 0.85rem;
}
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: 10px 12px;
background: #0a0a0a;
border: 1px solid #333;
border-radius: 6px;
color: #e0e0e0;
font-size: 0.95rem;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #00d4ff;
}
.form-group textarea {
min-height: 80px;
resize: vertical;
}
.modal-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
/* Phase 2: Context Menu */
.context-menu {
position: absolute;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 6px;
padding: 6px 0;
z-index: 4000;
display: none;
min-width: 160px;
}
.context-menu.show {
display: block;
}
.context-menu-item {
padding: 8px 16px;
cursor: pointer;
font-size: 0.9rem;
color: #e0e0e0;
}
.context-menu-item:hover {
background: #2a2a2a;
}
.context-menu-divider {
height: 1px;
background: #333;
margin: 6px 0;
}
/* Phase 2: Relation Editor */
.relation-editor {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #333;
}
.relation-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #0a0a0a;
border-radius: 4px;
margin-bottom: 8px;
font-size: 0.85rem;
}
.relation-item button {
background: transparent;
border: none;
color: #ff6b6b;
cursor: pointer;
font-size: 0.8rem;
}
/* Phase 2: Selection toolbar */
.selection-toolbar {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 10px 20px;
display: none;
gap: 10px;
z-index: 3500;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
}
.selection-toolbar.show {
display: flex;
}
/* Graph node styles */
.node-circle {
cursor: pointer;
}
.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>
<div class="header">
<div class="header-left">
<a href="/" class="back-link">← 返回项目列表</a>
<span class="project-name" id="projectName">加载中...</span>
</div>
<div class="header-actions">
<button class="btn btn-small" onclick="showUpload()">+ 上传文件</button>
</div>
</div>
<div class="main">
<!-- 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>
<!-- 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>
<!-- 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>
<!-- Upload Modal -->
<div class="upload-overlay" id="uploadOverlay">
<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>
</div>
<!-- Entity Editor Modal -->
<div class="modal-overlay" id="entityModal">
<div class="modal">
<h3 class="modal-header">编辑实体</h3>
<div class="form-group">
<label>实体名称</label>
<input type="text" id="entityName" placeholder="实体名称">
</div>
<div class="form-group">
<label>实体类型</label>
<select id="entityType">
<option value="PROJECT">项目 (PROJECT)</option>
<option value="TECH">技术 (TECH)</option>
<option value="PERSON">人物 (PERSON)</option>
<option value="ORG">组织 (ORG)</option>
<option value="OTHER">其他 (OTHER)</option>
</select>
</div>
<div class="form-group">
<label>定义描述</label>
<textarea id="entityDefinition" placeholder="一句话描述这个实体..."></textarea>
</div>
<div class="form-group">
<label>别名 (用逗号分隔)</label>
<input type="text" id="entityAliases" placeholder="别名1, 别名2, 别名3">
</div>
<div class="relation-editor" id="relationEditor" style="display:none;">
<h4 style="color:#888;font-size:0.9rem;margin-bottom:12px;">实体关系</h4>
<div id="relationList"></div>
<button class="btn-icon" onclick="showAddRelation()" style="margin-top:8px;">+ 添加关系</button>
</div>
<div class="modal-actions">
<button class="btn btn-danger" onclick="deleteEntity()" id="deleteEntityBtn">删除</button>
<button class="btn btn-secondary" onclick="hideEntityModal()">取消</button>
<button class="btn" onclick="saveEntity()">保存</button>
</div>
</div>
</div>
<!-- Add Relation Modal -->
<div class="modal-overlay" id="relationModal">
<div class="modal">
<h3 class="modal-header">添加关系</h3>
<div class="form-group">
<label>目标实体</label>
<select id="relationTarget"></select>
</div>
<div class="form-group">
<label>关系类型</label>
<select id="relationType">
<option value="belongs_to">属于 (belongs_to)</option>
<option value="works_with">合作 (works_with)</option>
<option value="depends_on">依赖 (depends_on)</option>
<option value="mentions">提及 (mentions)</option>
<option value="related">相关 (related)</option>
</select>
</div>
<div class="form-group">
<label>关系证据/说明</label>
<textarea id="relationEvidence" placeholder="描述这个关系的依据..."></textarea>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="hideRelationModal()">取消</button>
<button class="btn" onclick="saveRelation()">添加</button>
</div>
</div>
</div>
<!-- Merge Entities Modal -->
<div class="modal-overlay" id="mergeModal">
<div class="modal">
<h3 class="modal-header">合并实体</h3>
<p style="color:#888;margin-bottom:16px;font-size:0.9rem;">将选中的实体合并到目标实体中</p>
<div class="form-group">
<label>源实体</label>
<input type="text" id="mergeSource" disabled>
</div>
<div class="form-group">
<label>目标实体 (保留)</label>
<select id="mergeTarget"></select>
</div>
<div class="modal-actions">
<button class="btn btn-secondary" onclick="hideMergeModal()">取消</button>
<button class="btn" onclick="confirmMerge()">合并</button>
</div>
</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>
<div class="context-menu-item" onclick="showMergeModal()">🔄 合并实体</div>
<div class="context-menu-divider"></div>
<div class="context-menu-item" onclick="createEntityFromSelection()"> 标记为实体</div>
</div>
<!-- Selection Toolbar -->
<div class="selection-toolbar" id="selectionToolbar">
<span style="color:#888;font-size:0.85rem;">选中文本:</span>
<button class="btn-icon" onclick="createEntityFromSelection()">标记为实体</button>
<button class="btn-icon" onclick="hideSelectionToolbar()">取消</button>
</div>
<script src="app.js"></script>
</body>
</html>