MicroFish/frontend/src/components/WorkbenchPanel.vue

658 lines
16 KiB
Vue
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.

<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">图谱构建</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 API 构建知识图谱,提取实体和关系。
</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="description">图谱构建已完成,请进入下一步进行环境参数配置。</p>
<button
class="action-btn"
:disabled="currentPhase < 2"
@click="$emit('next-step')"
>
进入环境搭建 ➝
</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'
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 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;
text-transform: uppercase;
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: #FFF3E0; color: #EF6C00; }
.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>