refactor(wizard): bifurcation MainView selon route.query.mode + i18n Private

- MainView.vue lit route.query.mode (public|private) et bifurque après graph build
- État Private remonté dans MainView (privateForm, privateSimStatus, timers...)
- Cleanup timers sur onBeforeRouteLeave + onBeforeRouteUpdate + onUnmounted
- currentStep reset sur changement de mode via watcher isPrivateMode
- Step1GraphBuild reçoit mode prop ('public'|'private'), gate createSimulation+router.push en Public, emit('next-step') en Private
- Ajout clés i18n public.stepNames, public.modeBadge, private.stepNames, private.modeBadge (EN + ZH)
- Hardcode "PRIVATE IMPACT" et arrays privateStepNames/privateBreadcrumb migrés vers tm()

Prompts N°24 + N°27 — Roadmap refactoring wizard

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Cyril 2026-04-17 21:10:50 +02:00
parent e4fe3f91aa
commit 75d5a9bf64
4 changed files with 751 additions and 84 deletions

View File

@ -201,10 +201,11 @@ const props = defineProps({
ontologyProgress: Object, ontologyProgress: Object,
buildProgress: Object, buildProgress: Object,
graphData: Object, graphData: Object,
systemLogs: { type: Array, default: () => [] } systemLogs: { type: Array, default: () => [] },
mode: { type: String, default: 'public' }
}) })
defineEmits(['next-step']) const emit = defineEmits(['next-step'])
const selectedOntologyItem = ref(null) const selectedOntologyItem = ref(null)
const logContent = ref(null) const logContent = ref(null)
@ -217,6 +218,12 @@ const handleEnterEnvSetup = async () => {
return return
} }
// Mode privé : pas de simulation publique, on passe directement à l'étape 2 (Requirement)
if (props.mode === 'private') {
emit('next-step')
return
}
creatingSimulation.value = true creatingSimulation.value = true
try { try {

View File

@ -1,12 +1,19 @@
<template> <template>
<div class="main-view"> <div class="main-view" :class="{ 'is-private-mode': isPrivateMode }">
<!-- Header --> <!-- Header -->
<header class="app-header"> <header class="app-header">
<div class="header-left"> <div class="header-left">
<div class="brand" @click="router.push('/')">MIROFISH</div> <div class="brand" @click="router.push('/')">MIROFISH</div>
<div v-if="isPrivateMode" class="mode-badge">
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>
{{ t('private.modeBadge') }}
</div>
</div> </div>
<div class="header-center"> <div class="header-center" v-if="!isPrivateMode">
<div class="view-switcher"> <div class="view-switcher">
<button <button
v-for="mode in ['graph', 'split', 'workbench']" v-for="mode in ['graph', 'split', 'workbench']"
@ -24,8 +31,8 @@
<LanguageSwitcher /> <LanguageSwitcher />
<div class="step-divider"></div> <div class="step-divider"></div>
<div class="workflow-step"> <div class="workflow-step">
<span class="step-num">Step {{ currentStep }}/5</span> <span class="step-num">Step {{ currentStep }}/{{ currentStepNames.length }}</span>
<span class="step-name">{{ $tm('main.stepNames')[currentStep - 1] }}</span> <span class="step-name">{{ currentStepNames[currentStep - 1] }}</span>
</div> </div>
<div class="step-divider"></div> <div class="step-divider"></div>
<span class="status-indicator" :class="statusClass"> <span class="status-indicator" :class="statusClass">
@ -35,8 +42,10 @@
</div> </div>
</header> </header>
<!-- Main Content Area --> <!--
<main class="content-area"> MODE PUBLIC Graph + Step Panel
-->
<main v-if="!isPrivateMode" class="content-area">
<!-- Left Panel: Graph --> <!-- Left Panel: Graph -->
<div class="panel-wrapper left" :style="leftPanelStyle"> <div class="panel-wrapper left" :style="leftPanelStyle">
<GraphPanel <GraphPanel
@ -53,6 +62,7 @@
<!-- Step 1: 图谱构建 --> <!-- Step 1: 图谱构建 -->
<Step1GraphBuild <Step1GraphBuild
v-if="currentStep === 1" v-if="currentStep === 1"
mode="public"
:currentPhase="currentPhase" :currentPhase="currentPhase"
:projectData="projectData" :projectData="projectData"
:ontologyProgress="ontologyProgress" :ontologyProgress="ontologyProgress"
@ -73,48 +83,259 @@
/> />
</div> </div>
</main> </main>
<!--
MODE PRIVATE Bifurcation complète
-->
<template v-else>
<!-- Step breadcrumb (private) -->
<div class="steps-bar" v-if="currentStep >= 2">
<div
v-for="(name, idx) in privateBreadcrumb"
:key="idx"
class="step-node"
:class="{
'is-active': currentStep === idx + 2,
'is-done': currentStep > idx + 2,
}"
@click="currentStep > idx + 2 ? goToPrivateStep(idx + 2) : null"
>
<div class="step-circle">
<svg v-if="currentStep > idx + 2" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="3">
<polyline points="20 6 9 17 4 12" />
</svg>
<span v-else>{{ idx + 1 }}</span>
</div>
<span class="step-node-name">{{ name }}</span>
<div v-if="idx < privateBreadcrumb.length - 1" class="step-connector" :class="{ 'is-done': currentStep > idx + 2 }"></div>
</div>
</div>
<!-- Error banner (private) -->
<div v-if="privateError" class="error-banner">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
</svg>
{{ privateError }}
<button class="error-close" @click="privateError = null">×</button>
</div>
<!-- Step 1 Graph build (commun, mode=private) -->
<main v-if="currentStep === 1" class="content-area split-view">
<div class="panel-wrapper left" style="width:50%;">
<GraphPanel
:graphData="graphData"
:loading="graphLoading"
:currentPhase="currentPhase"
@refresh="refreshGraph"
@toggle-maximize="toggleMaximize('graph')"
/>
</div>
<div class="panel-wrapper right" style="width:50%;">
<Step1GraphBuild
mode="private"
:currentPhase="currentPhase"
:projectData="projectData"
:ontologyProgress="ontologyProgress"
:buildProgress="buildProgress"
:graphData="graphData"
:systemLogs="systemLogs"
@next-step="handleNextStep"
/>
</div>
</main>
<!-- Step 2 Private Requirement (Decision form + Prepare results) -->
<main v-else-if="currentStep === 2" class="content-area private-area">
<div v-if="!privatePrepareReady">
<Step2PrivateDecision
:form="privateForm"
:agentCounts="privateAgentCounts"
:projectId="currentProjectId"
:projectData="projectData"
@prepare="runPrivatePrepare"
/>
</div>
<div v-else class="centered-panel">
<div v-if="privateIsLoading" class="loading-block">
<div class="loading-ring"></div>
<p class="loading-label">Generating relational profiles and behavioural parameters</p>
<p class="loading-hint">This may take a few seconds per agent. The LLM is building the simulation config.</p>
</div>
<div v-else-if="privatePrepareResult" class="prepare-results">
<div class="result-badge result-badge--ok">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3">
<polyline points="20 6 9 17 4 12" />
</svg>
Simulation ready
</div>
<div class="result-stats">
<div class="stat-card">
<span class="stat-value mono">{{ privatePrepareResult.agent_count }}</span>
<span class="stat-label">Agents generated</span>
</div>
<div class="stat-card">
<span class="stat-value mono">{{ privateForm.horizonDays }}d</span>
<span class="stat-label">Temporal horizon</span>
</div>
<div class="stat-card">
<span class="stat-value mono">{{ privateForm.relationalTypes.length }}</span>
<span class="stat-label">Relation types</span>
</div>
</div>
<div class="relation-tags">
<span
v-for="t in privateForm.relationalTypes"
:key="t"
class="relation-tag"
>{{ t }}</span>
</div>
<div class="sim-id-block">
<span class="sim-id-label">SIM ID</span>
<span class="sim-id-value mono">{{ privateSimId }}</span>
</div>
<div class="result-actions">
<button class="btn-secondary" @click="privatePrepareReady = false; privatePrepareResult = null"> Back</button>
<button class="btn-primary" @click="runPrivateStart">
Launch Simulation
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3" />
</svg>
</button>
</div>
</div>
</div>
</main>
<!-- Step 3 Private Sim Running -->
<main v-else-if="currentStep === 3" class="content-area private-area">
<Step3PrivateSim
:simStatus="privateSimStatus"
:recentActions="privateRecentActions"
@stop="handlePrivateStop"
@report="runPrivateReport"
/>
</main>
<!-- Step 4 Private Report -->
<main v-else-if="currentStep === 4" class="content-area private-area">
<Step4PrivateReport
:reportResult="privateReportResult"
:isLoading="privateIsLoading"
:reportProgress="privateReportProgress"
:simId="privateSimId"
@retry="runPrivateReport"
@next="goToPrivateStep(5)"
/>
</main>
<!-- Step 5 Private Interaction -->
<main v-else-if="currentStep === 5" class="content-area private-area">
<Step5PrivateInteraction
:simId="privateSimId"
:chatAgents="privateChatAgents"
/>
</main>
</template>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue' import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import GraphPanel from '../components/GraphPanel.vue' import GraphPanel from '../components/GraphPanel.vue'
import Step1GraphBuild from '../components/Step1GraphBuild.vue' import Step1GraphBuild from '../components/Step1GraphBuild.vue'
import Step2EnvSetup from '../components/Step2EnvSetup.vue' import Step2EnvSetup from '../components/Step2EnvSetup.vue'
import { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData } from '../api/graph' import Step2PrivateDecision from '../components/private/Step2PrivateDecision.vue'
import { getPendingUpload, clearPendingUpload } from '../store/pendingUpload' import Step3PrivateSim from '../components/private/Step3PrivateSim.vue'
import Step4PrivateReport from '../components/private/Step4PrivateReport.vue'
import Step5PrivateInteraction from '../components/private/Step5PrivateInteraction.vue'
import LanguageSwitcher from '../components/LanguageSwitcher.vue' import LanguageSwitcher from '../components/LanguageSwitcher.vue'
import { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData } from '../api/graph'
import { getReport } from '../api/report.js'
import {
preparePrivateSimulation,
startPrivateSimulation,
getPrivateStatus,
stopPrivateSimulation,
getPrivateActions,
generatePrivateReport,
getPrivateReportStatus,
} from '../api/private.js'
import { getPendingUpload, clearPendingUpload } from '../store/pendingUpload'
import { RELATIONAL_TYPE_LABELS } from '../constants/private.js'
import { buildRequirement } from '../utils/private.js'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const { t, tm } = useI18n() const { t, tm } = useI18n()
// Layout State // Mode detection
const viewMode = ref('split') // graph | split | workbench const isPrivateMode = computed(() => route.query.mode === 'private')
// Step State // Layout State
const currentStep = ref(1) // 1: , 2: , 3: , 4: , 5: const viewMode = ref('split')
const stepNames = computed(() => tm('main.stepNames'))
// Data State // Step State
const currentStep = ref(1)
const publicStepNames = computed(() => tm('public.stepNames'))
const privateStepNames = computed(() => tm('private.stepNames'))
const privateBreadcrumb = computed(() => privateStepNames.value.slice(1))
const currentStepNames = computed(() => isPrivateMode.value ? privateStepNames.value : publicStepNames.value)
const stepNames = publicStepNames
// Data State (commun Step 1)
const currentProjectId = ref(route.params.projectId) const currentProjectId = ref(route.params.projectId)
const loading = ref(false) const loading = ref(false)
const graphLoading = ref(false) const graphLoading = ref(false)
const error = ref('') const error = ref('')
const projectData = ref(null) const projectData = ref(null)
const graphData = ref(null) const graphData = ref(null)
const currentPhase = ref(-1) // -1: Upload, 0: Ontology, 1: Build, 2: Complete const currentPhase = ref(-1)
const ontologyProgress = ref(null) const ontologyProgress = ref(null)
const buildProgress = ref(null) const buildProgress = ref(null)
const systemLogs = ref([]) const systemLogs = ref([])
// Polling timers // Public polling timers
let pollTimer = null let pollTimer = null
let graphPollTimer = null let graphPollTimer = null
// --- Computed Layout Styles --- // Private State
const privateSimId = ref(null)
const privateSimStatus = ref(null)
const privatePrepareResult = ref(null)
const privatePrepareReady = ref(false)
const privateReportResult = ref(null)
const privateIsLoading = ref(false)
const privateError = ref(null)
const privateReportProgress = ref('')
const privateRecentActions = ref([])
const privateChatAgents = ref([])
const privateForm = reactive({
decisionMakerName: '',
decisionMakerRole: '',
decisionMakerCompany: '',
decisionText: '',
decisionContext: '',
relationalTypes: ['ouvrier_production', 'technicien', 'commercial', 'manager', 'codir'],
horizonDays: 3,
questionsToMeasure: '',
})
const privateAgentCounts = reactive({})
// Private polling timers
let privatePollingTimer = null
let privateReportPollingTimer = null
// Computed Layout Styles
const leftPanelStyle = computed(() => { const leftPanelStyle = computed(() => {
if (viewMode.value === 'graph') return { width: '100%', opacity: 1, transform: 'translateX(0)' } if (viewMode.value === 'graph') return { width: '100%', opacity: 1, transform: 'translateX(0)' }
if (viewMode.value === 'workbench') return { width: '0%', opacity: 0, transform: 'translateX(-20px)' } if (viewMode.value === 'workbench') return { width: '0%', opacity: 0, transform: 'translateX(-20px)' }
@ -127,14 +348,35 @@ const rightPanelStyle = computed(() => {
return { width: '50%', opacity: 1, transform: 'translateX(0)' } return { width: '50%', opacity: 1, transform: 'translateX(0)' }
}) })
// --- Status Computed --- // Status Computed
const statusClass = computed(() => { const statusClass = computed(() => {
if (isPrivateMode.value) {
const s = privateSimStatus.value?.runner_status
if (s === 'running') return 'processing'
if (s === 'completed') return 'completed'
if (s === 'failed') return 'error'
if (privateIsLoading.value) return 'processing'
if (privateError.value || error.value) return 'error'
if (currentPhase.value >= 2) return 'completed'
return 'processing'
}
if (error.value) return 'error' if (error.value) return 'error'
if (currentPhase.value >= 2) return 'completed' if (currentPhase.value >= 2) return 'completed'
return 'processing' return 'processing'
}) })
const statusText = computed(() => { const statusText = computed(() => {
if (isPrivateMode.value) {
if (privateIsLoading.value) return 'Processing'
const s = privateSimStatus.value?.runner_status
if (s === 'running') return 'Running'
if (s === 'completed') return 'Completed'
if (s === 'failed') return 'Failed'
if (currentPhase.value >= 2) return 'Ready'
if (currentPhase.value === 1) return 'Building Graph'
if (currentPhase.value === 0) return 'Generating Ontology'
return 'Initializing'
}
if (error.value) return 'Error' if (error.value) return 'Error'
if (currentPhase.value >= 2) return 'Ready' if (currentPhase.value >= 2) return 'Ready'
if (currentPhase.value === 1) return 'Building Graph' if (currentPhase.value === 1) return 'Building Graph'
@ -142,32 +384,24 @@ const statusText = computed(() => {
return 'Initializing' return 'Initializing'
}) })
// --- Helpers --- // Helpers
const addLog = (msg) => { 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') 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 }) systemLogs.value.push({ time, msg })
// Keep last 100 logs if (systemLogs.value.length > 100) systemLogs.value.shift()
if (systemLogs.value.length > 100) {
systemLogs.value.shift()
}
} }
// --- Layout Methods --- // Layout Methods
const toggleMaximize = (target) => { const toggleMaximize = (target) => {
if (viewMode.value === target) { if (viewMode.value === target) viewMode.value = 'split'
viewMode.value = 'split' else viewMode.value = target
} else {
viewMode.value = target
}
} }
const handleNextStep = (params = {}) => { const handleNextStep = (params = {}) => {
if (currentStep.value < 5) { if (currentStep.value < 5) {
currentStep.value++ currentStep.value++
addLog(t('log.enterStep', { step: currentStep.value, name: stepNames.value[currentStep.value - 1] })) addLog(t('log.enterStep', { step: currentStep.value, name: currentStepNames.value[currentStep.value - 1] }))
if (!isPrivateMode.value && currentStep.value === 3 && params.maxRounds) {
// Step 2 Step 3
if (currentStep.value === 3 && params.maxRounds) {
addLog(t('log.customSimRounds', { rounds: params.maxRounds })) addLog(t('log.customSimRounds', { rounds: params.maxRounds }))
} }
} }
@ -176,12 +410,15 @@ const handleNextStep = (params = {}) => {
const handleGoBack = () => { const handleGoBack = () => {
if (currentStep.value > 1) { if (currentStep.value > 1) {
currentStep.value-- currentStep.value--
addLog(t('log.returnToStep', { step: currentStep.value, name: stepNames.value[currentStep.value - 1] })) addLog(t('log.returnToStep', { step: currentStep.value, name: currentStepNames.value[currentStep.value - 1] }))
} }
} }
// --- Data Logic --- const goToPrivateStep = (n) => {
currentStep.value = n
}
// Data Logic (commune Step 1)
const initProject = async () => { const initProject = async () => {
addLog('Project view initialized.') addLog('Project view initialized.')
if (currentProjectId.value === 'new') { if (currentProjectId.value === 'new') {
@ -215,14 +452,13 @@ const handleNewProject = async () => {
currentProjectId.value = res.data.project_id currentProjectId.value = res.data.project_id
projectData.value = res.data projectData.value = res.data
const pendingMode = route.query.mode const queryMode = route.query.mode
if (pendingMode === 'private') { router.replace({
await startBuildGraph() name: 'Process',
router.push(`/private/${res.data.project_id}`) params: { projectId: res.data.project_id },
return query: queryMode ? { mode: queryMode } : {},
} })
router.replace({ name: 'Process', params: { projectId: res.data.project_id } })
ontologyProgress.value = null ontologyProgress.value = null
addLog(`Ontology generated successfully for project ${res.data.project_id}`) addLog(`Ontology generated successfully for project ${res.data.project_id}`)
await startBuildGraph() await startBuildGraph()
@ -273,10 +509,10 @@ const loadProject = async () => {
const updatePhaseByStatus = (status) => { const updatePhaseByStatus = (status) => {
switch (status) { switch (status) {
case 'created': case 'created':
case 'ontology_generated': currentPhase.value = 0; break; case 'ontology_generated': currentPhase.value = 0; break
case 'graph_building': currentPhase.value = 1; break; case 'graph_building': currentPhase.value = 1; break
case 'graph_completed': currentPhase.value = 2; break; case 'graph_completed': currentPhase.value = 2; break
case 'failed': error.value = 'Project failed'; break; case 'failed': error.value = 'Project failed'; break
} }
} }
@ -309,7 +545,6 @@ const startGraphPolling = () => {
const fetchGraphData = async () => { const fetchGraphData = async () => {
try { try {
// Refresh project info to check for graph_id
const projRes = await getProject(currentProjectId.value) const projRes = await getProject(currentProjectId.value)
if (projRes.success && projRes.data.graph_id) { if (projRes.success && projRes.data.graph_id) {
const gRes = await getGraphData(projRes.data.graph_id) const gRes = await getGraphData(projRes.data.graph_id)
@ -336,7 +571,6 @@ const pollTaskStatus = async (taskId) => {
if (res.success) { if (res.success) {
const task = res.data const task = res.data
// Log progress message if it changed
if (task.message && task.message !== buildProgress.value?.message) { if (task.message && task.message !== buildProgress.value?.message) {
addLog(task.message) addLog(task.message)
} }
@ -346,14 +580,13 @@ const pollTaskStatus = async (taskId) => {
if (task.status === 'completed') { if (task.status === 'completed') {
addLog('Graph build task completed.') addLog('Graph build task completed.')
stopPolling() stopPolling()
stopGraphPolling() // Stop polling, do final load stopGraphPolling()
currentPhase.value = 2 currentPhase.value = 2
// Final load
const projRes = await getProject(currentProjectId.value) const projRes = await getProject(currentProjectId.value)
if (projRes.success && projRes.data.graph_id) { if (projRes.success && projRes.data.graph_id) {
projectData.value = projRes.data projectData.value = projRes.data
await loadGraph(projRes.data.graph_id) await loadGraph(projRes.data.graph_id)
} }
} else if (task.status === 'failed') { } else if (task.status === 'failed') {
stopPolling() stopPolling()
@ -392,27 +625,218 @@ const refreshGraph = () => {
} }
const stopPolling = () => { const stopPolling = () => {
if (pollTimer) { if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
clearInterval(pollTimer)
pollTimer = null
}
} }
const stopGraphPolling = () => { const stopGraphPolling = () => {
if (graphPollTimer) { if (graphPollTimer) { clearInterval(graphPollTimer); graphPollTimer = null; addLog('Graph polling stopped.') }
clearInterval(graphPollTimer) }
graphPollTimer = null
addLog('Graph polling stopped.') // Private Flow
const runPrivatePrepare = async () => {
if (!projectData.value?.graph_id) {
privateError.value = 'No graph_id found for this project. Build the graph first.'
return
}
privateError.value = null
privateIsLoading.value = true
privatePrepareReady.value = true
try {
const res = await preparePrivateSimulation({
graph_id: projectData.value.graph_id,
simulation_requirement: buildRequirement(privateForm, privateAgentCounts, RELATIONAL_TYPE_LABELS),
decision_context: privateForm.decisionContext,
entity_types: privateForm.relationalTypes,
})
privateSimId.value = res.data.sim_id
privatePrepareResult.value = res.data
} catch (e) {
privateError.value = `Prepare failed: ${e.message}`
privatePrepareReady.value = false
} finally {
privateIsLoading.value = false
} }
} }
const runPrivateStart = async () => {
privateError.value = null
privateIsLoading.value = true
currentStep.value = 3
try {
await startPrivateSimulation({ sim_id: privateSimId.value })
startPrivatePolling()
} catch (e) {
privateError.value = `Start failed: ${e.message}`
currentStep.value = 2
} finally {
privateIsLoading.value = false
}
}
const startPrivatePolling = () => {
privatePollingTimer = setInterval(pollPrivateStatus, 3000)
pollPrivateStatus()
}
const stopPrivatePolling = () => {
if (privatePollingTimer) { clearInterval(privatePollingTimer); privatePollingTimer = null }
}
const pollPrivateStatus = async () => {
if (!privateSimId.value) return
try {
const res = await getPrivateStatus(privateSimId.value)
privateSimStatus.value = res.data
privateRecentActions.value = res.data.recent_actions || []
const status = res.data.runner_status
if (status === 'completed' || status === 'stopped' || status === 'failed') {
stopPrivatePolling()
try {
const actRes = await getPrivateActions(privateSimId.value)
privateRecentActions.value = actRes.data || []
} catch { /* keep last */ }
}
} catch (e) {
console.error('Private status poll error:', e)
}
}
const handlePrivateStop = async () => {
try {
stopPrivatePolling()
await stopPrivateSimulation(privateSimId.value)
const res = await getPrivateStatus(privateSimId.value)
privateSimStatus.value = res.data
} catch (e) {
privateError.value = `Stop failed: ${e.message}`
}
}
const runPrivateReport = async () => {
privateError.value = null
privateIsLoading.value = true
privateReportProgress.value = 'Initialising Report Agent…'
currentStep.value = 4
try {
const res = await generatePrivateReport(privateSimId.value)
const reportId = res.data.report_id
const taskId = res.data.task_id
if (res.data.already_generated) {
const fullRes = await getReport(reportId)
privateReportResult.value = fullRes.data
privateIsLoading.value = false
return
}
startPrivateReportPolling(taskId, reportId)
} catch (e) {
privateError.value = `Report trigger failed: ${e.message}`
privateIsLoading.value = false
}
}
const startPrivateReportPolling = (taskId, reportId) => {
privateReportPollingTimer = setInterval(() => pollPrivateReport(taskId, reportId), 4000)
pollPrivateReport(taskId, reportId)
}
const stopPrivateReportPolling = () => {
if (privateReportPollingTimer) { clearInterval(privateReportPollingTimer); privateReportPollingTimer = null }
}
const pollPrivateReport = async (taskId, reportId) => {
try {
const res = await getPrivateReportStatus(taskId)
const status = res.data?.status
privateReportProgress.value = res.data?.message || 'Generating…'
if (status === 'completed') {
stopPrivateReportPolling()
const finalReportId = res.data?.result?.report_id || reportId
const fullRes = await getReport(finalReportId)
privateReportResult.value = fullRes.data
privateIsLoading.value = false
} else if (status === 'failed') {
stopPrivateReportPolling()
privateError.value = `Report failed: ${res.data?.error || res.data?.message}`
privateIsLoading.value = false
}
} catch (e) {
console.error('Private report poll error:', e)
}
}
const loadPrivateChatAgents = async () => {
try {
const res = await getPrivateActions(privateSimId.value)
const agentMap = {}
for (const action of (res.data || [])) {
if (!agentMap[action.agent_id]) {
agentMap[action.agent_id] = {
agent_id: action.agent_id,
entity_name: action.agent_name || `Agent ${action.agent_id}`,
relational_link_type: action.action_args?.relational_link_type || '',
stance: action.action_args?.stance || 'neutral',
}
}
}
privateChatAgents.value = Object.values(agentMap)
} catch (e) {
console.error('Could not load agents:', e)
}
}
watch(() => currentStep.value, (step) => {
if (isPrivateMode.value && step === 5 && privateChatAgents.value.length === 0) {
loadPrivateChatAgents()
}
})
// Timer Cleanup
const cleanupPublicTimers = () => {
stopPolling()
stopGraphPolling()
}
const cleanupPrivateTimers = () => {
stopPrivatePolling()
stopPrivateReportPolling()
}
const cleanupAllTimers = () => {
cleanupPublicTimers()
cleanupPrivateTimers()
}
// Mode watcher (reset transient state when mode changes)
watch(isPrivateMode, () => {
currentStep.value = 1
privatePrepareReady.value = false
cleanupPrivateTimers()
})
// Lifecycle
onMounted(() => { onMounted(() => {
initProject() initProject()
}) })
onBeforeRouteLeave(() => {
cleanupAllTimers()
})
onBeforeRouteUpdate((to, from) => {
if (to.params.projectId !== from.params.projectId) {
cleanupAllTimers()
}
})
onUnmounted(() => { onUnmounted(() => {
stopPolling() cleanupAllTimers()
stopGraphPolling()
}) })
</script> </script>
@ -439,6 +863,12 @@ onUnmounted(() => {
position: relative; position: relative;
} }
.header-left {
display: flex;
align-items: center;
gap: 14px;
}
.header-center { .header-center {
position: absolute; position: absolute;
left: 50%; left: 50%;
@ -453,6 +883,19 @@ onUnmounted(() => {
cursor: pointer; cursor: pointer;
} }
.mode-badge {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.14em;
background: #000;
color: #fff;
padding: 4px 10px;
border-radius: 2px;
}
.view-switcher { .view-switcher {
display: flex; display: flex;
background: #F5F5F5; background: #F5F5F5;
@ -539,6 +982,10 @@ onUnmounted(() => {
overflow: hidden; overflow: hidden;
} }
.content-area.split-view {
display: flex;
}
.panel-wrapper { .panel-wrapper {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
@ -549,4 +996,201 @@ onUnmounted(() => {
.panel-wrapper.left { .panel-wrapper.left {
border-right: 1px solid #EAEAEA; border-right: 1px solid #EAEAEA;
} }
/* ── Private area ─────────────────────────────────────────────────────── */
.private-area {
display: block;
overflow-y: auto;
padding: 24px 32px;
}
/* Steps bar (private) */
.steps-bar {
display: flex;
align-items: center;
padding: 12px 32px;
border-bottom: 1px solid #EFEFEF;
background: #FAFAFA;
flex-shrink: 0;
gap: 0;
}
.step-node {
display: flex;
align-items: center;
gap: 8px;
position: relative;
}
.step-circle {
width: 24px;
height: 24px;
border-radius: 50%;
border: 1.5px solid #D0D0D0;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
color: #999;
flex-shrink: 0;
transition: all 0.2s;
}
.step-node.is-active .step-circle { border-color: #000; background: #000; color: #fff; }
.step-node.is-done .step-circle { border-color: #4CAF50; background: #4CAF50; color: #fff; }
.step-node-name {
font-size: 11px;
font-weight: 600;
color: #AAA;
white-space: nowrap;
}
.step-node.is-active .step-node-name { color: #000; }
.step-node.is-done { cursor: pointer; }
.step-node.is-done .step-node-name { color: #555; }
.step-connector {
width: 32px;
height: 1.5px;
background: #E0E0E0;
margin: 0 8px;
flex-shrink: 0;
}
.step-connector.is-done { background: #4CAF50; }
/* Error banner */
.error-banner {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 32px;
background: #FFF3F3;
border-bottom: 1px solid #FFCDD2;
font-size: 12px;
color: #C62828;
}
.error-close {
margin-left: auto;
background: none;
border: none;
font-size: 16px;
cursor: pointer;
color: #C62828;
padding: 0 4px;
}
/* Private centered panel (step 2 after prepare) */
.centered-panel {
max-width: 680px;
margin: 0 auto;
}
.loading-block {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 60px 0;
text-align: center;
}
.loading-ring {
width: 40px;
height: 40px;
border: 3px solid #E5E7EB;
border-top-color: #000;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.loading-label { font-size: 14px; font-weight: 600; color: #000; }
.loading-hint { font-size: 12px; color: #888; max-width: 400px; line-height: 1.5; }
.prepare-results { display: flex; flex-direction: column; gap: 20px; padding: 20px 0; }
.result-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
padding: 6px 12px;
border-radius: 2px;
}
.result-badge--ok { background: #E8F5E9; color: #2E7D32; }
.result-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.stat-card {
border: 1.5px solid #E8E8E8;
border-radius: 4px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-value { font-size: 28px; font-weight: 700; color: #000; }
.stat-label { font-size: 10px; font-weight: 600; letter-spacing: 0.1em; color: #888; text-transform: uppercase; }
.relation-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.relation-tag { font-size: 11px; padding: 3px 8px; background: #F0F0F0; border-radius: 2px; color: #444; font-weight: 500; text-transform: capitalize; }
.sim-id-block { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: #F7F7F7; border-radius: 3px; }
.sim-id-label { font-size: 10px; font-weight: 700; letter-spacing: 0.1em; color: #999; }
.sim-id-value { font-size: 12px; color: #333; }
.result-actions { display: flex; gap: 10px; }
.btn-primary {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 22px;
background: #000;
color: #fff;
border: none;
border-radius: 3px;
font-size: 13px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: background 0.15s;
}
.btn-primary:hover { background: #222; }
.btn-primary:disabled { background: #CCC; cursor: not-allowed; }
.btn-secondary {
display: flex;
align-items: center;
gap: 6px;
padding: 9px 18px;
background: #fff;
color: #000;
border: 1.5px solid #000;
border-radius: 3px;
font-size: 13px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: background 0.15s;
}
.btn-secondary:hover { background: #F5F5F5; }
.mono { font-family: 'JetBrains Mono', monospace; }
</style> </style>

View File

@ -77,6 +77,14 @@
"layoutWorkbench": "Workbench", "layoutWorkbench": "Workbench",
"stepNames": ["Graph Build", "Env Setup", "Run Simulation", "Report Generation", "Deep Interaction"] "stepNames": ["Graph Build", "Env Setup", "Run Simulation", "Report Generation", "Deep Interaction"]
}, },
"public": {
"stepNames": ["Graph Build", "Env Setup", "Run Simulation", "Report Generation", "Deep Interaction"],
"modeBadge": "PUBLIC OPINION"
},
"private": {
"stepNames": ["Requirement", "Prepare", "Run", "Report", "Interact"],
"modeBadge": "PRIVATE IMPACT"
},
"step1": { "step1": {
"ontologyGeneration": "Ontology Generation", "ontologyGeneration": "Ontology Generation",
"ontologyCompleted": "Completed", "ontologyCompleted": "Completed",

View File

@ -77,6 +77,14 @@
"layoutWorkbench": "工作台", "layoutWorkbench": "工作台",
"stepNames": ["图谱构建", "环境搭建", "开始模拟", "报告生成", "深度互动"] "stepNames": ["图谱构建", "环境搭建", "开始模拟", "报告生成", "深度互动"]
}, },
"public": {
"stepNames": ["图谱构建", "环境搭建", "开始模拟", "报告生成", "深度互动"],
"modeBadge": "公共舆论"
},
"private": {
"stepNames": ["需求", "准备", "运行", "报告", "互动"],
"modeBadge": "私域影响"
},
"step1": { "step1": {
"ontologyGeneration": "本体生成", "ontologyGeneration": "本体生成",
"ontologyCompleted": "已完成", "ontologyCompleted": "已完成",