704 lines
18 KiB
Vue
704 lines
18 KiB
Vue
<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>
|