Add GraphPanel and WorkbenchPanel components for enhanced visualization and interaction
- Introduced GraphPanel.vue for dynamic graph visualization, featuring real-time updates, node and edge detail panels, and loading states. - Added WorkbenchPanel.vue to manage ontology generation and graph building processes, including progress tracking and system logs. - Updated MainView.vue to integrate both panels, allowing users to switch between graph and workbench views seamlessly. - Enhanced styling across components for improved user experience and visual consistency.
This commit is contained in:
465
frontend/src/views/MainView.vue
Normal file
465
frontend/src/views/MainView.vue
Normal file
@@ -0,0 +1,465 @@
|
||||
<template>
|
||||
<div class="main-view">
|
||||
<!-- Header -->
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<div class="brand" @click="router.push('/')">MIROFISH</div>
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<div class="view-switcher">
|
||||
<button
|
||||
v-for="mode in ['graph', 'split', 'workbench']"
|
||||
:key="mode"
|
||||
class="switch-btn"
|
||||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ mode.toUpperCase() }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<!-- Optional user info or status -->
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
<span class="dot"></span>
|
||||
{{ statusText }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="content-area">
|
||||
<!-- Left Panel: Graph -->
|
||||
<div class="panel-wrapper left" :style="leftPanelStyle">
|
||||
<GraphPanel
|
||||
:graphData="graphData"
|
||||
:loading="graphLoading"
|
||||
:currentPhase="currentPhase"
|
||||
@refresh="refreshGraph"
|
||||
@toggle-maximize="toggleMaximize('graph')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Workbench -->
|
||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||
<WorkbenchPanel
|
||||
:currentPhase="currentPhase"
|
||||
:projectData="projectData"
|
||||
:ontologyProgress="ontologyProgress"
|
||||
:buildProgress="buildProgress"
|
||||
:graphData="graphData"
|
||||
:systemLogs="systemLogs"
|
||||
@next-step="handleNextStep"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import GraphPanel from '../components/GraphPanel.vue'
|
||||
import WorkbenchPanel from '../components/WorkbenchPanel.vue'
|
||||
import { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData } from '../api/graph'
|
||||
import { getPendingUpload, clearPendingUpload } from '../store/pendingUpload'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// Layout State
|
||||
const viewMode = ref('split') // graph | split | workbench
|
||||
|
||||
// Data State
|
||||
const currentProjectId = ref(route.params.projectId)
|
||||
const loading = ref(false)
|
||||
const graphLoading = ref(false)
|
||||
const error = ref('')
|
||||
const projectData = ref(null)
|
||||
const graphData = ref(null)
|
||||
const currentPhase = ref(-1) // -1: Upload, 0: Ontology, 1: Build, 2: Complete
|
||||
const ontologyProgress = ref(null)
|
||||
const buildProgress = ref(null)
|
||||
const systemLogs = ref([])
|
||||
|
||||
// Polling timers
|
||||
let pollTimer = null
|
||||
let graphPollTimer = null
|
||||
|
||||
// --- Computed Layout Styles ---
|
||||
const leftPanelStyle = computed(() => {
|
||||
if (viewMode.value === 'graph') return { width: '100%', opacity: 1, transform: 'translateX(0)' }
|
||||
if (viewMode.value === 'workbench') return { width: '0%', opacity: 0, transform: 'translateX(-20px)' }
|
||||
return { width: '50%', opacity: 1, transform: 'translateX(0)' }
|
||||
})
|
||||
|
||||
const rightPanelStyle = computed(() => {
|
||||
if (viewMode.value === 'workbench') return { width: '100%', opacity: 1, transform: 'translateX(0)' }
|
||||
if (viewMode.value === 'graph') return { width: '0%', opacity: 0, transform: 'translateX(20px)' }
|
||||
return { width: '50%', opacity: 1, transform: 'translateX(0)' }
|
||||
})
|
||||
|
||||
// --- Status Computed ---
|
||||
const statusClass = computed(() => {
|
||||
if (error.value) return 'error'
|
||||
if (currentPhase.value >= 2) return 'completed'
|
||||
return 'processing'
|
||||
})
|
||||
|
||||
const statusText = computed(() => {
|
||||
if (error.value) return 'Error'
|
||||
if (currentPhase.value >= 2) return 'Ready'
|
||||
if (currentPhase.value === 1) return 'Building Graph'
|
||||
if (currentPhase.value === 0) return 'Generating Ontology'
|
||||
return 'Initializing'
|
||||
})
|
||||
|
||||
// --- Helpers ---
|
||||
const addLog = (msg) => {
|
||||
const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) + '.' + new Date().getMilliseconds().toString().padStart(3, '0')
|
||||
systemLogs.value.push({ time, msg })
|
||||
// Keep last 100 logs
|
||||
if (systemLogs.value.length > 100) {
|
||||
systemLogs.value.shift()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Layout Methods ---
|
||||
const toggleMaximize = (target) => {
|
||||
if (viewMode.value === target) {
|
||||
viewMode.value = 'split'
|
||||
} else {
|
||||
viewMode.value = target
|
||||
}
|
||||
}
|
||||
|
||||
const handleNextStep = () => {
|
||||
alert('Environment Setup coming soon...')
|
||||
}
|
||||
|
||||
// --- Data Logic ---
|
||||
|
||||
const initProject = async () => {
|
||||
addLog('Project view initialized.')
|
||||
if (currentProjectId.value === 'new') {
|
||||
await handleNewProject()
|
||||
} else {
|
||||
await loadProject()
|
||||
}
|
||||
}
|
||||
|
||||
const handleNewProject = async () => {
|
||||
const pending = getPendingUpload()
|
||||
if (!pending.isPending || pending.files.length === 0) {
|
||||
error.value = 'No pending files found.'
|
||||
addLog('Error: No pending files found for new project.')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
currentPhase.value = 0
|
||||
ontologyProgress.value = { message: 'Uploading and analyzing docs...' }
|
||||
addLog('Starting ontology generation: Uploading files...')
|
||||
|
||||
const formData = new FormData()
|
||||
pending.files.forEach(f => formData.append('files', f))
|
||||
formData.append('simulation_requirement', pending.simulationRequirement)
|
||||
|
||||
const res = await generateOntology(formData)
|
||||
if (res.success) {
|
||||
clearPendingUpload()
|
||||
currentProjectId.value = res.data.project_id
|
||||
projectData.value = res.data
|
||||
|
||||
router.replace({ name: 'Process', params: { projectId: res.data.project_id } })
|
||||
ontologyProgress.value = null
|
||||
addLog(`Ontology generated successfully for project ${res.data.project_id}`)
|
||||
await startBuildGraph()
|
||||
} else {
|
||||
error.value = res.error || 'Ontology generation failed'
|
||||
addLog(`Error generating ontology: ${error.value}`)
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
addLog(`Exception in handleNewProject: ${err.message}`)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadProject = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
addLog(`Loading project ${currentProjectId.value}...`)
|
||||
const res = await getProject(currentProjectId.value)
|
||||
if (res.success) {
|
||||
projectData.value = res.data
|
||||
updatePhaseByStatus(res.data.status)
|
||||
addLog(`Project loaded. Status: ${res.data.status}`)
|
||||
|
||||
if (res.data.status === 'ontology_generated' && !res.data.graph_id) {
|
||||
await startBuildGraph()
|
||||
} else if (res.data.status === 'graph_building' && res.data.graph_build_task_id) {
|
||||
currentPhase.value = 1
|
||||
startPollingTask(res.data.graph_build_task_id)
|
||||
startGraphPolling()
|
||||
} else if (res.data.status === 'graph_completed' && res.data.graph_id) {
|
||||
currentPhase.value = 2
|
||||
await loadGraph(res.data.graph_id)
|
||||
}
|
||||
} else {
|
||||
error.value = res.error
|
||||
addLog(`Error loading project: ${res.error}`)
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
addLog(`Exception in loadProject: ${err.message}`)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const updatePhaseByStatus = (status) => {
|
||||
switch (status) {
|
||||
case 'created':
|
||||
case 'ontology_generated': currentPhase.value = 0; break;
|
||||
case 'graph_building': currentPhase.value = 1; break;
|
||||
case 'graph_completed': currentPhase.value = 2; break;
|
||||
case 'failed': error.value = 'Project failed'; break;
|
||||
}
|
||||
}
|
||||
|
||||
const startBuildGraph = async () => {
|
||||
try {
|
||||
currentPhase.value = 1
|
||||
buildProgress.value = { progress: 0, message: 'Starting build...' }
|
||||
addLog('Initiating graph build...')
|
||||
|
||||
const res = await buildGraph({ project_id: currentProjectId.value })
|
||||
if (res.success) {
|
||||
addLog(`Graph build task started. Task ID: ${res.data.task_id}`)
|
||||
startGraphPolling()
|
||||
startPollingTask(res.data.task_id)
|
||||
} else {
|
||||
error.value = res.error
|
||||
addLog(`Error starting build: ${res.error}`)
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = err.message
|
||||
addLog(`Exception in startBuildGraph: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const startGraphPolling = () => {
|
||||
addLog('Started polling for graph data...')
|
||||
fetchGraphData()
|
||||
graphPollTimer = setInterval(fetchGraphData, 10000)
|
||||
}
|
||||
|
||||
const fetchGraphData = async () => {
|
||||
try {
|
||||
// Refresh project info to check for graph_id
|
||||
const projRes = await getProject(currentProjectId.value)
|
||||
if (projRes.success && projRes.data.graph_id) {
|
||||
const gRes = await getGraphData(projRes.data.graph_id)
|
||||
if (gRes.success) {
|
||||
graphData.value = gRes.data
|
||||
const nodeCount = gRes.data.node_count || gRes.data.nodes?.length || 0
|
||||
const edgeCount = gRes.data.edge_count || gRes.data.edges?.length || 0
|
||||
addLog(`Graph data refreshed. Nodes: ${nodeCount}, Edges: ${edgeCount}`)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Graph fetch error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const startPollingTask = (taskId) => {
|
||||
pollTaskStatus(taskId)
|
||||
pollTimer = setInterval(() => pollTaskStatus(taskId), 2000)
|
||||
}
|
||||
|
||||
const pollTaskStatus = async (taskId) => {
|
||||
try {
|
||||
const res = await getTaskStatus(taskId)
|
||||
if (res.success) {
|
||||
const task = res.data
|
||||
|
||||
// Log progress message if it changed
|
||||
if (task.message && task.message !== buildProgress.value?.message) {
|
||||
addLog(task.message)
|
||||
}
|
||||
|
||||
buildProgress.value = { progress: task.progress || 0, message: task.message }
|
||||
|
||||
if (task.status === 'completed') {
|
||||
addLog('Graph build task completed.')
|
||||
stopPolling()
|
||||
stopGraphPolling() // Stop polling, do final load
|
||||
currentPhase.value = 2
|
||||
|
||||
// Final load
|
||||
const projRes = await getProject(currentProjectId.value)
|
||||
if (projRes.success && projRes.data.graph_id) {
|
||||
projectData.value = projRes.data
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
}
|
||||
} else if (task.status === 'failed') {
|
||||
stopPolling()
|
||||
error.value = task.error
|
||||
addLog(`Graph build task failed: ${task.error}`)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
const loadGraph = async (graphId) => {
|
||||
graphLoading.value = true
|
||||
addLog(`Loading full graph data: ${graphId}`)
|
||||
try {
|
||||
const res = await getGraphData(graphId)
|
||||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
addLog('Graph data loaded successfully.')
|
||||
} else {
|
||||
addLog(`Failed to load graph data: ${res.error}`)
|
||||
}
|
||||
} catch (e) {
|
||||
addLog(`Exception loading graph: ${e.message}`)
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshGraph = () => {
|
||||
if (projectData.value?.graph_id) {
|
||||
addLog('Manual graph refresh triggered.')
|
||||
loadGraph(projectData.value.graph_id)
|
||||
}
|
||||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
const stopGraphPolling = () => {
|
||||
if (graphPollTimer) {
|
||||
clearInterval(graphPollTimer)
|
||||
graphPollTimer = null
|
||||
addLog('Graph polling stopped.')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initProject()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
stopGraphPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-view {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #FFF;
|
||||
overflow: hidden;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.app-header {
|
||||
height: 60px;
|
||||
border-bottom: 1px solid #EAEAEA;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24px;
|
||||
background: #FFF;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-weight: 800;
|
||||
font-size: 18px;
|
||||
letter-spacing: 1px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.view-switcher {
|
||||
display: flex;
|
||||
background: #F5F5F5;
|
||||
padding: 4px;
|
||||
border-radius: 6px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.switch-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding: 6px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.switch-btn.active {
|
||||
background: #FFF;
|
||||
color: #000;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #CCC;
|
||||
}
|
||||
|
||||
.status-indicator.processing .dot { background: #FF5722; animation: pulse 1s infinite; }
|
||||
.status-indicator.completed .dot { background: #4CAF50; }
|
||||
.status-indicator.error .dot { background: #F44336; }
|
||||
|
||||
@keyframes pulse { 50% { opacity: 0.5; } }
|
||||
|
||||
/* Content */
|
||||
.content-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-wrapper {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1), opacity 0.3s ease, transform 0.3s ease;
|
||||
will-change: width, opacity, transform;
|
||||
}
|
||||
|
||||
.panel-wrapper.left {
|
||||
border-right: 1px solid #EAEAEA;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user