feat(frontend): implement conditional polling and service-tier UI

This commit is contained in:
Armando Maynez 2026-05-03 02:18:28 -06:00
parent d790accebc
commit 744eb80fe1
5 changed files with 110 additions and 18 deletions

View File

@ -1435,7 +1435,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@ -1913,7 +1912,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -2053,7 +2051,6 @@
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@ -2128,7 +2125,6 @@
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.25.tgz",
"integrity": "sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.25",
"@vue/compiler-sfc": "3.5.25",

View File

@ -10,6 +10,7 @@
</div>
<div class="step-status">
<span v-if="currentPhase > 0" class="badge success">{{ $t('step1.ontologyCompleted') }}</span>
<span v-else-if="errorMsg && currentPhase === 0" class="badge error">FAILED</span>
<span v-else-if="currentPhase === 0" class="badge processing">{{ $t('step1.ontologyGenerating') }}</span>
<span v-else class="badge pending">{{ $t('step1.ontologyPending') }}</span>
</div>
@ -22,10 +23,16 @@
</p>
<!-- Loading / Progress -->
<div v-if="currentPhase === 0 && ontologyProgress" class="progress-section">
<div v-if="currentPhase === 0 && ontologyProgress && !errorMsg" class="progress-section">
<div class="spinner-sm"></div>
<span>{{ ontologyProgress.message || $t('step1.analyzingDocs') }}</span>
</div>
<!-- Error Alert for Phase 0 -->
<div v-if="currentPhase === 0 && errorMsg" class="error-alert">
<span class="error-icon"></span>
<span class="error-text">{{ errorMsg }}</span>
</div>
<!-- Detail Overlay -->
<div v-if="selectedOntologyItem" class="ontology-detail-overlay">
@ -114,6 +121,7 @@
</div>
<div class="step-status">
<span v-if="currentPhase > 1" class="badge success">{{ $t('step1.ontologyCompleted') }}</span>
<span v-else-if="errorMsg && currentPhase === 1" class="badge error">FAILED</span>
<span v-else-if="currentPhase === 1" class="badge processing">{{ buildProgress?.progress || 0 }}%</span>
<span v-else class="badge pending">{{ $t('step1.ontologyPending') }}</span>
</div>
@ -125,6 +133,12 @@
{{ $t('step1.graphRagDesc') }}
</p>
<!-- Error Alert for Phase 1 -->
<div v-if="currentPhase === 1 && errorMsg" class="error-alert" style="margin-bottom: 16px;">
<span class="error-icon"></span>
<span class="error-text">{{ errorMsg }}</span>
</div>
<!-- Stats Cards -->
<div class="stats-grid">
<div class="stat-card">
@ -201,7 +215,8 @@ const props = defineProps({
ontologyProgress: Object,
buildProgress: Object,
graphData: Object,
systemLogs: { type: Array, default: () => [] }
systemLogs: { type: Array, default: () => [] },
errorMsg: { type: String, default: '' }
})
defineEmits(['next-step'])
@ -349,6 +364,7 @@ watch(() => props.systemLogs.length, () => {
.badge.processing { background: #FF5722; color: #FFF; }
.badge.accent { background: #FF5722; color: #FFF; }
.badge.pending { background: #F5F5F5; color: #999; }
.badge.error { background: #FFEBEE; color: #D32F2F; }
.api-note {
font-family: 'JetBrains Mono', monospace;
@ -364,6 +380,28 @@ watch(() => props.systemLogs.length, () => {
margin-bottom: 16px;
}
.error-alert {
display: flex;
align-items: flex-start;
gap: 8px;
background: #FFF5F5;
border: 1px solid #FFCDD2;
border-radius: 6px;
padding: 12px;
margin-top: 8px;
}
.error-icon {
font-size: 14px;
}
.error-text {
font-size: 12px;
color: #C62828;
line-height: 1.4;
word-break: break-word;
}
/* Step 01 Tags */
.tags-container {
margin-top: 12px;

View File

@ -59,6 +59,7 @@
:buildProgress="buildProgress"
:graphData="graphData"
:systemLogs="systemLogs"
:errorMsg="error"
@next-step="handleNextStep"
/>
<!-- Step 2: 环境搭建 -->
@ -83,7 +84,7 @@ 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 { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData, getGraphConfig } from '../api/graph'
import { getPendingUpload, clearPendingUpload } from '../store/pendingUpload'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
@ -114,6 +115,9 @@ const systemLogs = ref([])
let pollTimer = null
let graphPollTimer = null
// Graph polling config (fetched from backend)
const graphPollInterval = ref(0) // 0 = manual only
// --- Computed Layout Styles ---
const leftPanelStyle = computed(() => {
if (viewMode.value === 'graph') return { width: '100%', opacity: 1, transform: 'translateX(0)' }
@ -184,6 +188,19 @@ const handleGoBack = () => {
const initProject = async () => {
addLog('Project view initialized.')
// Fetch graph polling config from backend
try {
const configRes = await getGraphConfig()
if (configRes.success && configRes.data) {
graphPollInterval.value = configRes.data.poll_interval || 0
addLog(`Graph config loaded: poll_interval=${graphPollInterval.value}s, cache_ttl=${configRes.data.cache_ttl}s`)
}
} catch (err) {
addLog('Could not load graph config, defaulting to manual refresh only.')
graphPollInterval.value = 0
}
if (currentProjectId.value === 'new') {
await handleNewProject()
} else {
@ -295,9 +312,17 @@ const startBuildGraph = async () => {
}
const startGraphPolling = () => {
addLog('Started polling for graph data...')
// Always do one immediate fetch
fetchGraphData()
graphPollTimer = setInterval(fetchGraphData, 10000)
// Only set up automatic polling if poll_interval > 0 (paid plan)
if (graphPollInterval.value > 0) {
const intervalMs = graphPollInterval.value * 1000
addLog(`Started automatic graph polling (every ${graphPollInterval.value}s)...`)
graphPollTimer = setInterval(fetchGraphData, intervalMs)
} else {
addLog('Automatic graph polling disabled (FREE plan). Use manual refresh.')
}
}
const fetchGraphData = async () => {

View File

@ -414,7 +414,7 @@
<script setup>
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData } from '../api/graph'
import { generateOntology, getProject, buildGraph, getTaskStatus, getGraphData, getGraphConfig } from '../api/graph'
import { getPendingUpload, clearPendingUpload } from '../store/pendingUpload'
import * as d3 from 'd3'
@ -443,6 +443,9 @@ const graphSvg = ref(null)
//
let pollTimer = null
// Graph polling config (fetched from backend)
const graphPollInterval = ref(0) // 0 = manual only
//
const statusClass = computed(() => {
if (error.value) return 'error'
@ -554,6 +557,16 @@ const getPhaseStatusText = (phase) => {
const initProject = async () => {
const paramProjectId = route.params.projectId
// Fetch graph polling config from backend
try {
const configRes = await getGraphConfig()
if (configRes.success && configRes.data) {
graphPollInterval.value = configRes.data.poll_interval || 0
}
} catch (err) {
graphPollInterval.value = 0
}
if (paramProjectId === 'new') {
// store
await handleNewProject()
@ -715,10 +728,13 @@ const startGraphPolling = () => {
//
fetchGraphData()
// 10
graphPollTimer = setInterval(async () => {
await fetchGraphData()
}, 10000)
// Only set up automatic polling if poll_interval > 0 (paid plan)
if (graphPollInterval.value > 0) {
const intervalMs = graphPollInterval.value * 1000
graphPollTimer = setInterval(async () => {
await fetchGraphData()
}, intervalMs)
}
}
//

View File

@ -73,7 +73,7 @@ import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import GraphPanel from '../components/GraphPanel.vue'
import Step3Simulation from '../components/Step3Simulation.vue'
import { getProject, getGraphData } from '../api/graph'
import { getProject, getGraphData, getGraphConfig } from '../api/graph'
import { getSimulation, getSimulationConfig, stopSimulation, closeSimulationEnv, getEnvStatus } from '../api/simulation'
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
import { useI18n } from 'vue-i18n'
@ -100,6 +100,7 @@ const graphData = ref(null)
const graphLoading = ref(false)
const systemLogs = ref([])
const currentStatus = ref('processing') // processing | completed | error
const graphPollInterval = ref(0) // 0 = manual only
// --- Computed Layout Styles ---
const leftPanelStyle = computed(() => {
@ -208,6 +209,16 @@ const loadSimulationData = async () => {
try {
addLog(t('log.loadingSimData', { id: currentSimulationId.value }))
// Fetch graph polling config from backend
try {
const configRes = await getGraphConfig()
if (configRes.success && configRes.data) {
graphPollInterval.value = configRes.data.poll_interval || 0
}
} catch (err) {
graphPollInterval.value = 0
}
// simulation
const simRes = await getSimulation(currentSimulationId.value)
if (simRes.success && simRes.data) {
@ -278,9 +289,15 @@ let graphRefreshTimer = null
const startGraphRefresh = () => {
if (graphRefreshTimer) return
addLog(t('log.graphRealtimeRefreshStart'))
// 30
graphRefreshTimer = setInterval(refreshGraph, 30000)
// Only set up automatic refresh if poll_interval > 0 (paid plan)
if (graphPollInterval.value > 0) {
const intervalMs = Math.max(graphPollInterval.value * 1000, 30000) // At least 30s during simulation
addLog(t('log.graphRealtimeRefreshStart'))
graphRefreshTimer = setInterval(refreshGraph, intervalMs)
} else {
addLog('Automatic graph refresh disabled (FREE plan). Use manual refresh.')
}
}
const stopGraphRefresh = () => {