feat(frontend): implement conditional polling and service-tier UI
This commit is contained in:
parent
d790accebc
commit
744eb80fe1
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
// 手动刷新图谱
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue