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:
parent
e4fe3f91aa
commit
75d5a9bf64
|
|
@ -201,10 +201,11 @@ const props = defineProps({
|
|||
ontologyProgress: Object,
|
||||
buildProgress: 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 logContent = ref(null)
|
||||
|
|
@ -217,6 +218,12 @@ const handleEnterEnvSetup = async () => {
|
|||
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
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
<template>
|
||||
<div class="main-view">
|
||||
<div class="main-view" :class="{ 'is-private-mode': isPrivateMode }">
|
||||
<!-- Header -->
|
||||
<header class="app-header">
|
||||
<div class="header-left">
|
||||
<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 class="header-center">
|
||||
<div class="header-center" v-if="!isPrivateMode">
|
||||
<div class="view-switcher">
|
||||
<button
|
||||
v-for="mode in ['graph', 'split', 'workbench']"
|
||||
|
|
@ -24,8 +31,8 @@
|
|||
<LanguageSwitcher />
|
||||
<div class="step-divider"></div>
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step {{ currentStep }}/5</span>
|
||||
<span class="step-name">{{ $tm('main.stepNames')[currentStep - 1] }}</span>
|
||||
<span class="step-num">Step {{ currentStep }}/{{ currentStepNames.length }}</span>
|
||||
<span class="step-name">{{ currentStepNames[currentStep - 1] }}</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
|
|
@ -35,8 +42,10 @@
|
|||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="content-area">
|
||||
<!-- ══════════════════════════════════════════════════════════
|
||||
MODE PUBLIC — Graph + Step Panel
|
||||
══════════════════════════════════════════════════════════ -->
|
||||
<main v-if="!isPrivateMode" class="content-area">
|
||||
<!-- Left Panel: Graph -->
|
||||
<div class="panel-wrapper left" :style="leftPanelStyle">
|
||||
<GraphPanel
|
||||
|
|
@ -53,6 +62,7 @@
|
|||
<!-- Step 1: 图谱构建 -->
|
||||
<Step1GraphBuild
|
||||
v-if="currentStep === 1"
|
||||
mode="public"
|
||||
:currentPhase="currentPhase"
|
||||
:projectData="projectData"
|
||||
:ontologyProgress="ontologyProgress"
|
||||
|
|
@ -73,48 +83,259 @@
|
|||
/>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, reactive, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import GraphPanel from '../components/GraphPanel.vue'
|
||||
import Step1GraphBuild from '../components/Step1GraphBuild.vue'
|
||||
import Step2EnvSetup from '../components/Step2EnvSetup.vue'
|
||||
import { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData } from '../api/graph'
|
||||
import { getPendingUpload, clearPendingUpload } from '../store/pendingUpload'
|
||||
import Step2PrivateDecision from '../components/private/Step2PrivateDecision.vue'
|
||||
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 { 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 router = useRouter()
|
||||
const { t, tm } = useI18n()
|
||||
|
||||
// Layout State
|
||||
const viewMode = ref('split') // graph | split | workbench
|
||||
// ── Mode detection ─────────────────────────────────────────────────────────
|
||||
const isPrivateMode = computed(() => route.query.mode === 'private')
|
||||
|
||||
// Step State
|
||||
const currentStep = ref(1) // 1: 图谱构建, 2: 环境搭建, 3: 开始模拟, 4: 报告生成, 5: 深度互动
|
||||
const stepNames = computed(() => tm('main.stepNames'))
|
||||
// ── Layout State ──────────────────────────────────────────────────────────
|
||||
const viewMode = ref('split')
|
||||
|
||||
// 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 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 currentPhase = ref(-1)
|
||||
const ontologyProgress = ref(null)
|
||||
const buildProgress = ref(null)
|
||||
const systemLogs = ref([])
|
||||
|
||||
// Polling timers
|
||||
// Public polling timers
|
||||
let pollTimer = 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(() => {
|
||||
if (viewMode.value === 'graph') return { width: '100%', opacity: 1, transform: 'translateX(0)' }
|
||||
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)' }
|
||||
})
|
||||
|
||||
// --- Status Computed ---
|
||||
// ── Status 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 (currentPhase.value >= 2) return 'completed'
|
||||
return 'processing'
|
||||
})
|
||||
|
||||
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 (currentPhase.value >= 2) return 'Ready'
|
||||
if (currentPhase.value === 1) return 'Building Graph'
|
||||
|
|
@ -142,32 +384,24 @@ const statusText = computed(() => {
|
|||
return 'Initializing'
|
||||
})
|
||||
|
||||
// --- Helpers ---
|
||||
// ── 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()
|
||||
}
|
||||
if (systemLogs.value.length > 100) systemLogs.value.shift()
|
||||
}
|
||||
|
||||
// --- Layout Methods ---
|
||||
// ── Layout Methods ────────────────────────────────────────────────────────
|
||||
const toggleMaximize = (target) => {
|
||||
if (viewMode.value === target) {
|
||||
viewMode.value = 'split'
|
||||
} else {
|
||||
viewMode.value = target
|
||||
}
|
||||
if (viewMode.value === target) viewMode.value = 'split'
|
||||
else viewMode.value = target
|
||||
}
|
||||
|
||||
const handleNextStep = (params = {}) => {
|
||||
if (currentStep.value < 5) {
|
||||
currentStep.value++
|
||||
addLog(t('log.enterStep', { step: currentStep.value, name: stepNames.value[currentStep.value - 1] }))
|
||||
|
||||
// 如果是从 Step 2 进入 Step 3,记录模拟轮数配置
|
||||
if (currentStep.value === 3 && params.maxRounds) {
|
||||
addLog(t('log.enterStep', { step: currentStep.value, name: currentStepNames.value[currentStep.value - 1] }))
|
||||
if (!isPrivateMode.value && currentStep.value === 3 && params.maxRounds) {
|
||||
addLog(t('log.customSimRounds', { rounds: params.maxRounds }))
|
||||
}
|
||||
}
|
||||
|
|
@ -176,12 +410,15 @@ const handleNextStep = (params = {}) => {
|
|||
const handleGoBack = () => {
|
||||
if (currentStep.value > 1) {
|
||||
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 () => {
|
||||
addLog('Project view initialized.')
|
||||
if (currentProjectId.value === 'new') {
|
||||
|
|
@ -215,14 +452,13 @@ const handleNewProject = async () => {
|
|||
currentProjectId.value = res.data.project_id
|
||||
projectData.value = res.data
|
||||
|
||||
const pendingMode = route.query.mode
|
||||
if (pendingMode === 'private') {
|
||||
await startBuildGraph()
|
||||
router.push(`/private/${res.data.project_id}`)
|
||||
return
|
||||
}
|
||||
const queryMode = route.query.mode
|
||||
router.replace({
|
||||
name: 'Process',
|
||||
params: { projectId: res.data.project_id },
|
||||
query: queryMode ? { mode: queryMode } : {},
|
||||
})
|
||||
|
||||
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()
|
||||
|
|
@ -273,10 +509,10 @@ const loadProject = async () => {
|
|||
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;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -309,7 +545,6 @@ const startGraphPolling = () => {
|
|||
|
||||
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)
|
||||
|
|
@ -336,7 +571,6 @@ const pollTaskStatus = async (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)
|
||||
}
|
||||
|
|
@ -346,14 +580,13 @@ const pollTaskStatus = async (taskId) => {
|
|||
if (task.status === 'completed') {
|
||||
addLog('Graph build task completed.')
|
||||
stopPolling()
|
||||
stopGraphPolling() // Stop polling, do final load
|
||||
stopGraphPolling()
|
||||
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)
|
||||
projectData.value = projRes.data
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
}
|
||||
} else if (task.status === 'failed') {
|
||||
stopPolling()
|
||||
|
|
@ -392,27 +625,218 @@ const refreshGraph = () => {
|
|||
}
|
||||
|
||||
const stopPolling = () => {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
if (pollTimer) { clearInterval(pollTimer); pollTimer = null }
|
||||
}
|
||||
|
||||
const stopGraphPolling = () => {
|
||||
if (graphPollTimer) {
|
||||
clearInterval(graphPollTimer)
|
||||
graphPollTimer = null
|
||||
addLog('Graph polling stopped.')
|
||||
if (graphPollTimer) { 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(() => {
|
||||
initProject()
|
||||
})
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
cleanupAllTimers()
|
||||
})
|
||||
|
||||
onBeforeRouteUpdate((to, from) => {
|
||||
if (to.params.projectId !== from.params.projectId) {
|
||||
cleanupAllTimers()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
stopGraphPolling()
|
||||
cleanupAllTimers()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -439,6 +863,12 @@ onUnmounted(() => {
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
|
|
@ -453,6 +883,19 @@ onUnmounted(() => {
|
|||
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 {
|
||||
display: flex;
|
||||
background: #F5F5F5;
|
||||
|
|
@ -539,6 +982,10 @@ onUnmounted(() => {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-area.split-view {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.panel-wrapper {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
|
|
@ -549,4 +996,201 @@ onUnmounted(() => {
|
|||
.panel-wrapper.left {
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -77,6 +77,14 @@
|
|||
"layoutWorkbench": "Workbench",
|
||||
"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": {
|
||||
"ontologyGeneration": "Ontology Generation",
|
||||
"ontologyCompleted": "Completed",
|
||||
|
|
|
|||
|
|
@ -77,6 +77,14 @@
|
|||
"layoutWorkbench": "工作台",
|
||||
"stepNames": ["图谱构建", "环境搭建", "开始模拟", "报告生成", "深度互动"]
|
||||
},
|
||||
"public": {
|
||||
"stepNames": ["图谱构建", "环境搭建", "开始模拟", "报告生成", "深度互动"],
|
||||
"modeBadge": "公共舆论"
|
||||
},
|
||||
"private": {
|
||||
"stepNames": ["需求", "准备", "运行", "报告", "互动"],
|
||||
"modeBadge": "私域影响"
|
||||
},
|
||||
"step1": {
|
||||
"ontologyGeneration": "本体生成",
|
||||
"ontologyCompleted": "已完成",
|
||||
|
|
|
|||
Loading…
Reference in New Issue