MicroFish/frontend/src/components/Step1GraphBuild.vue

704 lines
18 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">{{ $t('step1.ontologyGeneration') }}</span>
</div>
<div class="step-status">
<span v-if="currentPhase > 0" class="badge success">{{ $t('step1.ontologyCompleted') }}</span>
<span v-else-if="currentPhase === 0" class="badge processing">{{ $t('step1.ontologyGenerating') }}</span>
<span v-else class="badge pending">{{ $t('step1.ontologyPending') }}</span>
</div>
</div>
<div class="card-content">
<p class="api-note">POST /api/graph/ontology/generate</p>
<p class="description">
{{ $t('step1.ontologyDesc') }}
</p>
<!-- Loading / Progress -->
<div v-if="currentPhase === 0 && ontologyProgress" class="progress-section">
<div class="spinner-sm"></div>
<span>{{ ontologyProgress.message || $t('step1.analyzingDocs') }}</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">{{ $t('step1.graphRagBuild') }}</span>
</div>
<div class="step-status">
<span v-if="currentPhase > 1" class="badge success">{{ $t('step1.ontologyCompleted') }}</span>
<span v-else-if="currentPhase === 1" class="badge processing">{{ buildProgress?.progress || 0 }}%</span>
<span v-else class="badge pending">{{ $t('step1.ontologyPending') }}</span>
</div>
</div>
<div class="card-content">
<p class="api-note">POST /api/graph/build</p>
<p class="description">
{{ $t('step1.graphRagDesc') }}
</p>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
<span class="stat-value">{{ graphStats.nodes }}</span>
<span class="stat-label">{{ $t('step1.entityNodes') }}</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ graphStats.edges }}</span>
<span class="stat-label">{{ $t('step1.relationEdges') }}</span>
</div>
<div class="stat-card">
<span class="stat-value">{{ graphStats.types }}</span>
<span class="stat-label">{{ $t('step1.schemaTypes') }}</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">{{ $t('step1.buildComplete') }}</span>
</div>
<div class="step-status">
<span v-if="currentPhase >= 2" class="badge accent">{{ $t('step1.inProgress') }}</span>
</div>
</div>
<div class="card-content">
<p class="api-note">POST /api/simulation/create</p>
<p class="description">{{ $t('step1.buildCompleteDesc') }}</p>
<button
class="action-btn"
:disabled="currentPhase < 2 || creatingSimulation"
@click="handleEnterEnvSetup"
>
<span v-if="creatingSimulation" class="spinner-sm"></span>
{{ creatingSimulation ? $t('step1.creating') : $t('step1.enterEnvSetup') + ' ➝' }}
</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 { useI18n } from 'vue-i18n'
import { createSimulation } from '../api/simulation'
const router = useRouter()
const { t } = useI18n()
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(t('step1.createSimulationFailed', { error: res.error || t('common.unknownError') }))
}
} catch (err) {
console.error('创建模拟异常:', err)
alert(t('step1.createSimulationException', { error: 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: var(--bg-secondary);
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: var(--bg-primary);
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
border: 1px solid var(--border);
transition: all 0.3s ease;
position: relative;
}
.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: var(--border-medium);
}
.step-card.active .step-num,
.step-card.completed .step-num {
color: var(--text-primary);
}
.step-title {
font-weight: 600;
font-size: 14px;
letter-spacing: 0.5px;
color: var(--text-primary);
}
.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: var(--bg-hover); color: var(--text-faint); }
.api-note {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
color: var(--text-faint);
margin-bottom: 8px;
}
.description {
font-size: 12px;
color: var(--text-muted);
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: var(--text-faint);
margin-bottom: 8px;
font-weight: 600;
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.entity-tag {
background: var(--bg-hover);
border: 1px solid var(--border);
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
color: var(--text-secondary);
font-family: 'JetBrains Mono', monospace;
transition: all 0.2s;
}
.entity-tag.clickable {
cursor: pointer;
}
.entity-tag.clickable:hover {
background: var(--border-medium);
border-color: var(--border-medium);
}
/* Ontology Detail Overlay */
.ontology-detail-overlay {
position: absolute;
top: 60px;
left: 20px;
right: 20px;
bottom: 20px;
background: var(--bg-surface);
backdrop-filter: blur(4px);
z-index: 10;
border: 1px solid var(--border);
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
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 var(--border);
background: var(--bg-secondary);
}
.detail-title-group {
display: flex;
align-items: center;
gap: 8px;
}
.detail-type-badge {
font-size: 9px;
font-weight: 700;
color: var(--bg-primary);
background: var(--text-primary);
padding: 2px 6px;
border-radius: 2px;
text-transform: uppercase;
}
.detail-name {
font-size: 14px;
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
color: var(--text-primary);
}
.close-btn {
background: none;
border: none;
font-size: 18px;
color: var(--text-faint);
cursor: pointer;
line-height: 1;
}
.close-btn:hover {
color: var(--text-secondary);
}
.detail-body {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.detail-desc {
font-size: 12px;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px dashed var(--border);
}
.detail-section {
margin-bottom: 16px;
}
.section-label {
display: block;
font-size: 10px;
font-weight: 600;
color: var(--text-faint);
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: var(--bg-secondary);
border-radius: 4px;
}
.attr-name {
font-family: 'JetBrains Mono', monospace;
font-weight: 600;
color: var(--text-primary);
}
.attr-type {
color: var(--text-faint);
font-size: 10px;
}
.attr-desc {
color: var(--text-muted);
flex: 1;
min-width: 150px;
}
.example-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.example-tag {
font-size: 11px;
background: var(--bg-primary);
border: 1px solid var(--border);
padding: 3px 8px;
border-radius: 12px;
color: var(--text-muted);
}
.conn-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
padding: 6px;
background: var(--bg-hover);
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
}
.conn-node {
font-weight: 600;
color: var(--text-secondary);
}
.conn-arrow {
color: var(--border-medium);
}
/* Step 02 Stats */
.stats-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 12px;
background: var(--bg-secondary);
padding: 16px;
border-radius: 6px;
}
.stat-card {
text-align: center;
}
.stat-value {
display: block;
font-size: 20px;
font-weight: 700;
color: var(--text-primary);
font-family: 'JetBrains Mono', monospace;
}
.stat-label {
font-size: 9px;
color: var(--text-faint);
text-transform: uppercase;
margin-top: 4px;
display: block;
}
/* Step 03 Button */
.action-btn {
width: 100%;
background: var(--text-primary);
color: var(--bg-primary);
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: var(--border-medium);
color: var(--text-muted);
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;
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>