Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="workbench-panel"> | |
| <div class="scroll-container"> | |
| <!-- Step 01: Ontology --> | |
| <div class="step-card" :class="{ 'active': currentPhase === 0, 'completed': currentPhase > 0 }"> | |
| <div class="card-header"> | |
| <div class="step-info"> | |
| <span class="step-num">01</span> | |
| <span class="step-title">本体生成</span> | |
| </div> | |
| <div class="step-status"> | |
| <span v-if="currentPhase > 0" class="badge success">已完成</span> | |
| <span v-else-if="currentPhase === 0" class="badge processing">生成中</span> | |
| <span v-else class="badge pending">等待</span> | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <p class="api-note">POST /api/graph/ontology/generate</p> | |
| <p class="description"> | |
| LLM分析文档内容与模拟需求,提取出现实种子,自动生成合适的本体结构 | |
| </p> | |
| <!-- Loading / Progress --> | |
| <div v-if="currentPhase === 0 && ontologyProgress" class="progress-section"> | |
| <div class="spinner-sm"></div> | |
| <span>{{ ontologyProgress.message || '正在分析文档...' }}</span> | |
| </div> | |
| <!-- Detail Overlay --> | |
| <div v-if="selectedOntologyItem" class="ontology-detail-overlay"> | |
| <div class="detail-header"> | |
| <div class="detail-title-group"> | |
| <span class="detail-type-badge">{{ selectedOntologyItem.itemType === 'entity' ? 'ENTITY' : 'RELATION' }}</span> | |
| <span class="detail-name">{{ selectedOntologyItem.name }}</span> | |
| </div> | |
| <button class="close-btn" @click="selectedOntologyItem = null">×</button> | |
| </div> | |
| <div class="detail-body"> | |
| <div class="detail-desc">{{ selectedOntologyItem.description }}</div> | |
| <!-- Attributes --> | |
| <div class="detail-section" v-if="selectedOntologyItem.attributes?.length"> | |
| <span class="section-label">ATTRIBUTES</span> | |
| <div class="attr-list"> | |
| <div v-for="attr in selectedOntologyItem.attributes" :key="attr.name" class="attr-item"> | |
| <span class="attr-name">{{ attr.name }}</span> | |
| <span class="attr-type">({{ attr.type }})</span> | |
| <span class="attr-desc">{{ attr.description }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Examples (Entity) --> | |
| <div class="detail-section" v-if="selectedOntologyItem.examples?.length"> | |
| <span class="section-label">EXAMPLES</span> | |
| <div class="example-list"> | |
| <span v-for="ex in selectedOntologyItem.examples" :key="ex" class="example-tag">{{ ex }}</span> | |
| </div> | |
| </div> | |
| <!-- Source/Target (Relation) --> | |
| <div class="detail-section" v-if="selectedOntologyItem.source_targets?.length"> | |
| <span class="section-label">CONNECTIONS</span> | |
| <div class="conn-list"> | |
| <div v-for="(conn, idx) in selectedOntologyItem.source_targets" :key="idx" class="conn-item"> | |
| <span class="conn-node">{{ conn.source }}</span> | |
| <span class="conn-arrow">→</span> | |
| <span class="conn-node">{{ conn.target }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Generated Entity Tags --> | |
| <div v-if="projectData?.ontology?.entity_types" class="tags-container" :class="{ 'dimmed': selectedOntologyItem }"> | |
| <span class="tag-label">GENERATED ENTITY TYPES</span> | |
| <div class="tags-list"> | |
| <span | |
| v-for="entity in projectData.ontology.entity_types" | |
| :key="entity.name" | |
| class="entity-tag clickable" | |
| @click="selectOntologyItem(entity, 'entity')" | |
| > | |
| {{ entity.name }} | |
| </span> | |
| </div> | |
| </div> | |
| <!-- Generated Relation Tags --> | |
| <div v-if="projectData?.ontology?.edge_types" class="tags-container" :class="{ 'dimmed': selectedOntologyItem }"> | |
| <span class="tag-label">GENERATED RELATION TYPES</span> | |
| <div class="tags-list"> | |
| <span | |
| v-for="rel in projectData.ontology.edge_types" | |
| :key="rel.name" | |
| class="entity-tag clickable" | |
| @click="selectOntologyItem(rel, 'relation')" | |
| > | |
| {{ rel.name }} | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 02: Graph Build --> | |
| <div class="step-card" :class="{ 'active': currentPhase === 1, 'completed': currentPhase > 1 }"> | |
| <div class="card-header"> | |
| <div class="step-info"> | |
| <span class="step-num">02</span> | |
| <span class="step-title">GraphRAG构建</span> | |
| </div> | |
| <div class="step-status"> | |
| <span v-if="currentPhase > 1" class="badge success">已完成</span> | |
| <span v-else-if="currentPhase === 1" class="badge processing">{{ buildProgress?.progress || 0 }}%</span> | |
| <span v-else class="badge pending">等待</span> | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <p class="api-note">POST /api/graph/build</p> | |
| <p class="description"> | |
| 基于生成的本体,将文档自动分块后调用 Zep 构建知识图谱,提取实体和关系,并形成时序记忆与社区摘要 | |
| </p> | |
| <!-- Stats Cards --> | |
| <div class="stats-grid"> | |
| <div class="stat-card"> | |
| <span class="stat-value">{{ graphStats.nodes }}</span> | |
| <span class="stat-label">实体节点</span> | |
| </div> | |
| <div class="stat-card"> | |
| <span class="stat-value">{{ graphStats.edges }}</span> | |
| <span class="stat-label">关系边</span> | |
| </div> | |
| <div class="stat-card"> | |
| <span class="stat-value">{{ graphStats.types }}</span> | |
| <span class="stat-label">SCHEMA类型</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Step 03: Complete --> | |
| <div class="step-card" :class="{ 'active': currentPhase === 2, 'completed': currentPhase >= 2 }"> | |
| <div class="card-header"> | |
| <div class="step-info"> | |
| <span class="step-num">03</span> | |
| <span class="step-title">构建完成</span> | |
| </div> | |
| <div class="step-status"> | |
| <span v-if="currentPhase >= 2" class="badge accent">进行中</span> | |
| </div> | |
| </div> | |
| <div class="card-content"> | |
| <p class="api-note">POST /api/simulation/create</p> | |
| <p class="description">图谱构建已完成,请进入下一步进行模拟环境搭建</p> | |
| <button | |
| class="action-btn" | |
| :disabled="currentPhase < 2 || creatingSimulation" | |
| @click="handleEnterEnvSetup" | |
| > | |
| <span v-if="creatingSimulation" class="spinner-sm"></span> | |
| {{ creatingSimulation ? '创建中...' : '进入环境搭建 ➝' }} | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Bottom Info / Logs --> | |
| <div class="system-logs"> | |
| <div class="log-header"> | |
| <span class="log-title">SYSTEM DASHBOARD</span> | |
| <span class="log-id">{{ projectData?.project_id || 'NO_PROJECT' }}</span> | |
| </div> | |
| <div class="log-content" ref="logContent"> | |
| <div class="log-line" v-for="(log, idx) in systemLogs" :key="idx"> | |
| <span class="log-time">{{ log.time }}</span> | |
| <span class="log-msg">{{ log.msg }}</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script setup> | |
| import { computed, ref, watch, nextTick } from 'vue' | |
| import { useRouter } from 'vue-router' | |
| import { createSimulation } from '../api/simulation' | |
| const router = useRouter() | |
| const props = defineProps({ | |
| currentPhase: { type: Number, default: 0 }, | |
| projectData: Object, | |
| ontologyProgress: Object, | |
| buildProgress: Object, | |
| graphData: Object, | |
| systemLogs: { type: Array, default: () => [] } | |
| }) | |
| defineEmits(['next-step']) | |
| const selectedOntologyItem = ref(null) | |
| const logContent = ref(null) | |
| const creatingSimulation = ref(false) | |
| // 进入环境搭建 - 创建 simulation 并跳转 | |
| const handleEnterEnvSetup = async () => { | |
| if (!props.projectData?.project_id || !props.projectData?.graph_id) { | |
| console.error('缺少项目或图谱信息') | |
| return | |
| } | |
| creatingSimulation.value = true | |
| try { | |
| const res = await createSimulation({ | |
| project_id: props.projectData.project_id, | |
| graph_id: props.projectData.graph_id, | |
| enable_twitter: true, | |
| enable_reddit: true | |
| }) | |
| if (res.success && res.data?.simulation_id) { | |
| // 跳转到 simulation 页面 | |
| router.push({ | |
| name: 'Simulation', | |
| params: { simulationId: res.data.simulation_id } | |
| }) | |
| } else { | |
| console.error('创建模拟失败:', res.error) | |
| alert('创建模拟失败: ' + (res.error || '未知错误')) | |
| } | |
| } catch (err) { | |
| console.error('创建模拟异常:', err) | |
| alert('创建模拟异常: ' + err.message) | |
| } finally { | |
| creatingSimulation.value = false | |
| } | |
| } | |
| const selectOntologyItem = (item, type) => { | |
| selectedOntologyItem.value = { ...item, itemType: type } | |
| } | |
| const graphStats = computed(() => { | |
| const nodes = props.graphData?.node_count || props.graphData?.nodes?.length || 0 | |
| const edges = props.graphData?.edge_count || props.graphData?.edges?.length || 0 | |
| const types = props.projectData?.ontology?.entity_types?.length || 0 | |
| return { nodes, edges, types } | |
| }) | |
| const formatDate = (dateStr) => { | |
| if (!dateStr) return '--:--:--' | |
| const d = new Date(dateStr) | |
| return d.toLocaleTimeString('en-US', { hour12: false }) + '.' + d.getMilliseconds() | |
| } | |
| // Auto-scroll logs | |
| watch(() => props.systemLogs.length, () => { | |
| nextTick(() => { | |
| if (logContent.value) { | |
| logContent.value.scrollTop = logContent.value.scrollHeight | |
| } | |
| }) | |
| }) | |
| </script> | |
| <style scoped> | |
| .workbench-panel { | |
| height: 100%; | |
| background-color: #FAFAFA; | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .scroll-container { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 24px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| .step-card { | |
| background: #FFF; | |
| border-radius: 8px; | |
| padding: 20px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.04); | |
| border: 1px solid #EAEAEA; | |
| transition: all 0.3s ease; | |
| position: relative; /* For absolute overlay */ | |
| } | |
| .step-card.active { | |
| border-color: #FF5722; | |
| box-shadow: 0 4px 12px rgba(255, 87, 34, 0.08); | |
| } | |
| .card-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 16px; | |
| } | |
| .step-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .step-num { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 20px; | |
| font-weight: 700; | |
| color: #E0E0E0; | |
| } | |
| .step-card.active .step-num, | |
| .step-card.completed .step-num { | |
| color: #000; | |
| } | |
| .step-title { | |
| font-weight: 600; | |
| font-size: 14px; | |
| letter-spacing: 0.5px; | |
| } | |
| .badge { | |
| font-size: 10px; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| } | |
| .badge.success { background: #E8F5E9; color: #2E7D32; } | |
| .badge.processing { background: #FF5722; color: #FFF; } | |
| .badge.accent { background: #FF5722; color: #FFF; } | |
| .badge.pending { background: #F5F5F5; color: #999; } | |
| .api-note { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 10px; | |
| color: #999; | |
| margin-bottom: 8px; | |
| } | |
| .description { | |
| font-size: 12px; | |
| color: #666; | |
| line-height: 1.5; | |
| margin-bottom: 16px; | |
| } | |
| /* Step 01 Tags */ | |
| .tags-container { | |
| margin-top: 12px; | |
| transition: opacity 0.3s; | |
| } | |
| .tags-container.dimmed { | |
| opacity: 0.3; | |
| pointer-events: none; | |
| } | |
| .tag-label { | |
| display: block; | |
| font-size: 10px; | |
| color: #AAA; | |
| margin-bottom: 8px; | |
| font-weight: 600; | |
| } | |
| .tags-list { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 8px; | |
| } | |
| .entity-tag { | |
| background: #F5F5F5; | |
| border: 1px solid #EEE; | |
| padding: 4px 10px; | |
| border-radius: 4px; | |
| font-size: 11px; | |
| color: #333; | |
| font-family: 'JetBrains Mono', monospace; | |
| transition: all 0.2s; | |
| } | |
| .entity-tag.clickable { | |
| cursor: pointer; | |
| } | |
| .entity-tag.clickable:hover { | |
| background: #E0E0E0; | |
| border-color: #CCC; | |
| } | |
| /* Ontology Detail Overlay */ | |
| .ontology-detail-overlay { | |
| position: absolute; | |
| top: 60px; /* Below header roughly */ | |
| left: 20px; | |
| right: 20px; | |
| bottom: 20px; | |
| background: rgba(255, 255, 255, 0.98); | |
| backdrop-filter: blur(4px); | |
| z-index: 10; | |
| border: 1px solid #EAEAEA; | |
| box-shadow: 0 4px 20px rgba(0,0,0,0.05); | |
| border-radius: 6px; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| animation: fadeIn 0.2s ease-out; | |
| } | |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } | |
| .detail-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 12px 16px; | |
| border-bottom: 1px solid #EAEAEA; | |
| background: #FAFAFA; | |
| } | |
| .detail-title-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .detail-type-badge { | |
| font-size: 9px; | |
| font-weight: 700; | |
| color: #FFF; | |
| background: #000; | |
| padding: 2px 6px; | |
| border-radius: 2px; | |
| text-transform: uppercase; | |
| } | |
| .detail-name { | |
| font-size: 14px; | |
| font-weight: 700; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .close-btn { | |
| background: none; | |
| border: none; | |
| font-size: 18px; | |
| color: #999; | |
| cursor: pointer; | |
| line-height: 1; | |
| } | |
| .close-btn:hover { | |
| color: #333; | |
| } | |
| .detail-body { | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 16px; | |
| } | |
| .detail-desc { | |
| font-size: 12px; | |
| color: #444; | |
| line-height: 1.5; | |
| margin-bottom: 16px; | |
| padding-bottom: 12px; | |
| border-bottom: 1px dashed #EAEAEA; | |
| } | |
| .detail-section { | |
| margin-bottom: 16px; | |
| } | |
| .section-label { | |
| display: block; | |
| font-size: 10px; | |
| font-weight: 600; | |
| color: #AAA; | |
| margin-bottom: 8px; | |
| } | |
| .attr-list, .conn-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .attr-item { | |
| font-size: 11px; | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| align-items: baseline; | |
| padding: 4px; | |
| background: #F9F9F9; | |
| border-radius: 4px; | |
| } | |
| .attr-name { | |
| font-family: 'JetBrains Mono', monospace; | |
| font-weight: 600; | |
| color: #000; | |
| } | |
| .attr-type { | |
| color: #999; | |
| font-size: 10px; | |
| } | |
| .attr-desc { | |
| color: #555; | |
| flex: 1; | |
| min-width: 150px; | |
| } | |
| .example-list { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| } | |
| .example-tag { | |
| font-size: 11px; | |
| background: #FFF; | |
| border: 1px solid #E0E0E0; | |
| padding: 3px 8px; | |
| border-radius: 12px; | |
| color: #555; | |
| } | |
| .conn-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 11px; | |
| padding: 6px; | |
| background: #F5F5F5; | |
| border-radius: 4px; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .conn-node { | |
| font-weight: 600; | |
| color: #333; | |
| } | |
| .conn-arrow { | |
| color: #BBB; | |
| } | |
| /* Step 02 Stats */ | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr 1fr; | |
| gap: 12px; | |
| background: #F9F9F9; | |
| padding: 16px; | |
| border-radius: 6px; | |
| } | |
| .stat-card { | |
| text-align: center; | |
| } | |
| .stat-value { | |
| display: block; | |
| font-size: 20px; | |
| font-weight: 700; | |
| color: #000; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| .stat-label { | |
| font-size: 9px; | |
| color: #999; | |
| text-transform: uppercase; | |
| margin-top: 4px; | |
| display: block; | |
| } | |
| /* Step 03 Button */ | |
| .action-btn { | |
| width: 100%; | |
| background: #000; | |
| color: #FFF; | |
| border: none; | |
| padding: 14px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: opacity 0.2s; | |
| } | |
| .action-btn:hover:not(:disabled) { | |
| opacity: 0.8; | |
| } | |
| .action-btn:disabled { | |
| background: #CCC; | |
| cursor: not-allowed; | |
| } | |
| .progress-section { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| font-size: 12px; | |
| color: #FF5722; | |
| margin-bottom: 12px; | |
| } | |
| .spinner-sm { | |
| width: 14px; | |
| height: 14px; | |
| border: 2px solid #FFCCBC; | |
| border-top-color: #FF5722; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| /* System Logs */ | |
| .system-logs { | |
| background: #000; | |
| color: #DDD; | |
| padding: 16px; | |
| font-family: 'JetBrains Mono', monospace; | |
| border-top: 1px solid #222; | |
| flex-shrink: 0; | |
| } | |
| .log-header { | |
| display: flex; | |
| justify-content: space-between; | |
| border-bottom: 1px solid #333; | |
| padding-bottom: 8px; | |
| margin-bottom: 8px; | |
| font-size: 10px; | |
| color: #888; | |
| } | |
| .log-content { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| height: 80px; /* Approx 4 lines visible */ | |
| overflow-y: auto; | |
| padding-right: 4px; | |
| } | |
| .log-content::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| .log-content::-webkit-scrollbar-thumb { | |
| background: #333; | |
| border-radius: 2px; | |
| } | |
| .log-line { | |
| font-size: 11px; | |
| display: flex; | |
| gap: 12px; | |
| line-height: 1.5; | |
| } | |
| .log-time { | |
| color: #666; | |
| min-width: 75px; | |
| } | |
| .log-msg { | |
| color: #CCC; | |
| word-break: break-all; | |
| } | |
| </style> | |