466 lines
12 KiB
Vue
466 lines
12 KiB
Vue
<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>
|