Merge pull request #428 from Ghostubborn/feat/i18n
feat(i18n): 添加多语言切换功能,支持中英文
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<script>document.documentElement.lang = localStorage.getItem('locale') || 'zh'</script>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@100..800&family=Noto+Sans+SC:wght@300;400;500;700;800;900&family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
|
||||
|
||||
83
frontend/package-lock.json
generated
83
frontend/package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"axios": "^1.13.2",
|
||||
"d3": "^7.9.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.3.0",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -506,6 +507,67 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.0.tgz",
|
||||
"integrity": "sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/devtools-types": "11.3.0",
|
||||
"@intlify/message-compiler": "11.3.0",
|
||||
"@intlify/shared": "11.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/devtools-types": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.0.tgz",
|
||||
"integrity": "sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.3.0",
|
||||
"@intlify/shared": "11.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.0.tgz",
|
||||
"integrity": "sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.3.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.0.tgz",
|
||||
"integrity": "sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
@@ -2035,6 +2097,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
|
||||
"integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.3.0",
|
||||
"@intlify/devtools-types": "11.3.0",
|
||||
"@intlify/shared": "11.3.0",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-router": {
|
||||
"version": "4.6.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.3.tgz",
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"axios": "1.13.2",
|
||||
"d3": "^7.9.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-i18n": "^11.3.0",
|
||||
"vue-router": "^4.6.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import i18n from '../i18n'
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
@@ -12,6 +13,7 @@ const service = axios.create({
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
config.headers['Accept-Language'] = i18n.global.locale.value
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<template>
|
||||
<div class="graph-panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">Graph Relationship Visualization</span>
|
||||
<span class="panel-title">{{ $t('graph.panelTitle') }}</span>
|
||||
<!-- 顶部工具栏 (Internal Top Right) -->
|
||||
<div class="header-tools">
|
||||
<button class="tool-btn" @click="$emit('refresh')" :disabled="loading" title="刷新图谱">
|
||||
<button class="tool-btn" @click="$emit('refresh')" :disabled="loading" :title="$t('graph.refreshGraph')">
|
||||
<span class="icon-refresh" :class="{ 'spinning': loading }">↻</span>
|
||||
<span class="btn-text">Refresh</span>
|
||||
</button>
|
||||
<button class="tool-btn" @click="$emit('toggle-maximize')" title="最大化/还原">
|
||||
<button class="tool-btn" @click="$emit('toggle-maximize')" :title="$t('graph.toggleMaximize')">
|
||||
<span class="icon-maximize">⛶</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -27,7 +27,7 @@
|
||||
<path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24 2.5 2.5 0 0 0-4.44-4.04z" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ isSimulating ? 'GraphRAG长短期记忆实时更新中' : '实时更新中...' }}
|
||||
{{ isSimulating ? $t('graph.graphMemoryRealtime') : $t('graph.realtimeUpdating') }}
|
||||
</div>
|
||||
|
||||
<!-- 模拟结束后的提示 -->
|
||||
@@ -39,8 +39,8 @@
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="hint-text">还有少量内容处理中,建议稍后手动刷新图谱</span>
|
||||
<button class="hint-close-btn" @click="dismissFinishedHint" title="关闭提示">
|
||||
<span class="hint-text">{{ $t('graph.pendingContentHint') }}</span>
|
||||
<button class="hint-close-btn" @click="dismissFinishedHint" :title="$t('graph.closeHint')">
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||
@@ -51,7 +51,7 @@
|
||||
<!-- 节点/边详情面板 -->
|
||||
<div v-if="selectedItem" class="detail-panel">
|
||||
<div class="detail-panel-header">
|
||||
<span class="detail-title">{{ selectedItem.type === 'node' ? 'Node Details' : 'Relationship' }}</span>
|
||||
<span class="detail-title">{{ selectedItem.type === 'node' ? $t('graph.nodeDetails') : $t('graph.relationship') }}</span>
|
||||
<span v-if="selectedItem.type === 'node'" class="detail-type-badge" :style="{ background: selectedItem.color, color: '#fff' }">
|
||||
{{ selectedItem.entityType }}
|
||||
</span>
|
||||
@@ -203,13 +203,13 @@
|
||||
<!-- 加载状态 -->
|
||||
<div v-else-if="loading" class="graph-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>图谱数据加载中...</p>
|
||||
<p>{{ $t('graph.graphDataLoading') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 等待/空状态 -->
|
||||
<div v-else class="graph-state">
|
||||
<div class="empty-icon">❖</div>
|
||||
<p class="empty-text">等待本体生成...</p>
|
||||
<p class="empty-text">{{ $t('graph.waitingOntology') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- 标题区域 -->
|
||||
<div class="section-header">
|
||||
<div class="section-line"></div>
|
||||
<span class="section-title">推演记录</span>
|
||||
<span class="section-title">{{ $t('history.title') }}</span>
|
||||
<div class="section-line"></div>
|
||||
</div>
|
||||
|
||||
@@ -36,16 +36,16 @@
|
||||
<span
|
||||
class="status-icon"
|
||||
:class="{ available: project.project_id, unavailable: !project.project_id }"
|
||||
title="图谱构建"
|
||||
:title="$t('history.graphBuild')"
|
||||
>◇</span>
|
||||
<span
|
||||
class="status-icon available"
|
||||
title="环境搭建"
|
||||
:title="$t('history.envSetup')"
|
||||
>◈</span>
|
||||
<span
|
||||
class="status-icon"
|
||||
:class="{ available: project.report_id, unavailable: !project.report_id }"
|
||||
title="分析报告"
|
||||
:title="$t('history.analysisReport')"
|
||||
>◆</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,13 +67,13 @@
|
||||
</div>
|
||||
<!-- 如果有更多文件,显示提示 -->
|
||||
<div v-if="project.files.length > 3" class="files-more">
|
||||
+{{ project.files.length - 3 }} 个文件
|
||||
{{ $t('history.moreFiles', { count: project.files.length - 3 }) }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 无文件时的占位 -->
|
||||
<div class="files-empty" v-else>
|
||||
<span class="empty-file-icon">◇</span>
|
||||
<span class="empty-file-text">暂无文件</span>
|
||||
<span class="empty-file-text">{{ $t('history.noFiles') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<span class="loading-spinner"></span>
|
||||
<span class="loading-text">加载中...</span>
|
||||
<span class="loading-text">{{ $t('history.loadingText') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 历史回放详情弹窗 -->
|
||||
@@ -126,27 +126,27 @@
|
||||
<div class="modal-body">
|
||||
<!-- 模拟需求 -->
|
||||
<div class="modal-section">
|
||||
<div class="modal-label">模拟需求</div>
|
||||
<div class="modal-requirement">{{ selectedProject.simulation_requirement || '无' }}</div>
|
||||
<div class="modal-label">{{ $t('history.simRequirement') }}</div>
|
||||
<div class="modal-requirement">{{ selectedProject.simulation_requirement || $t('common.none') }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<div class="modal-section">
|
||||
<div class="modal-label">关联文件</div>
|
||||
<div class="modal-label">{{ $t('history.relatedFiles') }}</div>
|
||||
<div class="modal-files" v-if="selectedProject.files && selectedProject.files.length > 0">
|
||||
<div v-for="(file, index) in selectedProject.files" :key="index" class="modal-file-item">
|
||||
<span class="file-tag" :class="getFileType(file.filename)">{{ getFileTypeLabel(file.filename) }}</span>
|
||||
<span class="modal-file-name">{{ file.filename }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-empty" v-else>暂无关联文件</div>
|
||||
<div class="modal-empty" v-else>{{ $t('history.noRelatedFiles') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推演回放分割线 -->
|
||||
<div class="modal-divider">
|
||||
<span class="divider-line"></span>
|
||||
<span class="divider-text">推演回放</span>
|
||||
<span class="divider-text">{{ $t('history.replayTitle') }}</span>
|
||||
<span class="divider-line"></span>
|
||||
</div>
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
>
|
||||
<span class="btn-step">Step1</span>
|
||||
<span class="btn-icon">◇</span>
|
||||
<span class="btn-text">图谱构建</span>
|
||||
<span class="btn-text">{{ $t('history.step1Button') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="modal-btn btn-simulation"
|
||||
@@ -167,7 +167,7 @@
|
||||
>
|
||||
<span class="btn-step">Step2</span>
|
||||
<span class="btn-icon">◈</span>
|
||||
<span class="btn-text">环境搭建</span>
|
||||
<span class="btn-text">{{ $t('history.step2Button') }}</span>
|
||||
</button>
|
||||
<button
|
||||
class="modal-btn btn-report"
|
||||
@@ -176,12 +176,12 @@
|
||||
>
|
||||
<span class="btn-step">Step4</span>
|
||||
<span class="btn-icon">◆</span>
|
||||
<span class="btn-text">分析报告</span>
|
||||
<span class="btn-text">{{ $t('history.step4Button') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 不可回放提示 -->
|
||||
<div class="modal-playback-hint">
|
||||
<span class="hint-text">Step3「开始模拟」与 Step5「深度互动」需在运行中启动,不支持历史回放</span>
|
||||
<span class="hint-text">{{ $t('history.replayHint') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -193,10 +193,12 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, onActivated, watch, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getSimulationHistory } from '../api/simulation'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
// 状态
|
||||
const projects = ref([])
|
||||
@@ -337,7 +339,7 @@ const truncateText = (text, maxLength) => {
|
||||
|
||||
// 从模拟需求生成标题(取前20字)
|
||||
const getSimulationTitle = (requirement) => {
|
||||
if (!requirement) return '未命名模拟'
|
||||
if (!requirement) return t('history.untitledSimulation')
|
||||
const title = requirement.slice(0, 20)
|
||||
return requirement.length > 20 ? title + '...' : title
|
||||
}
|
||||
@@ -353,8 +355,8 @@ const formatSimulationId = (simulationId) => {
|
||||
const formatRounds = (simulation) => {
|
||||
const current = simulation.current_round || 0
|
||||
const total = simulation.total_rounds || 0
|
||||
if (total === 0) return '未开始'
|
||||
return `${current}/${total} 轮`
|
||||
if (total === 0) return t('history.notStarted')
|
||||
return t('history.roundsProgress', { current, total })
|
||||
}
|
||||
|
||||
// 获取文件类型(用于样式)
|
||||
@@ -382,7 +384,7 @@ const getFileTypeLabel = (filename) => {
|
||||
|
||||
// 截断文件名(保留扩展名)
|
||||
const truncateFilename = (filename, maxLength) => {
|
||||
if (!filename) return '未知文件'
|
||||
if (!filename) return t('history.unknownFile')
|
||||
if (filename.length <= maxLength) return filename
|
||||
|
||||
const ext = filename.includes('.') ? '.' + filename.split('.').pop() : ''
|
||||
|
||||
124
frontend/src/components/LanguageSwitcher.vue
Normal file
124
frontend/src/components/LanguageSwitcher.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<div class="language-switcher" ref="switcherRef">
|
||||
<button class="switcher-trigger" @click="toggleDropdown">
|
||||
{{ currentLabel }}
|
||||
<span class="caret">{{ open ? '▲' : '▼' }}</span>
|
||||
</button>
|
||||
<ul v-if="open" class="switcher-dropdown">
|
||||
<li
|
||||
v-for="loc in availableLocales"
|
||||
:key="loc.key"
|
||||
class="switcher-option"
|
||||
:class="{ active: loc.key === locale }"
|
||||
@click="switchLocale(loc.key)"
|
||||
>
|
||||
{{ loc.label }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { availableLocales } from '@/i18n/index.js'
|
||||
|
||||
const { locale } = useI18n()
|
||||
const open = ref(false)
|
||||
const switcherRef = ref(null)
|
||||
|
||||
const currentLabel = computed(() => {
|
||||
const found = availableLocales.find(l => l.key === locale.value)
|
||||
return found ? found.label : locale.value
|
||||
})
|
||||
|
||||
const toggleDropdown = () => {
|
||||
open.value = !open.value
|
||||
}
|
||||
|
||||
const switchLocale = (key) => {
|
||||
locale.value = key
|
||||
localStorage.setItem('locale', key)
|
||||
document.documentElement.lang = key
|
||||
open.value = false
|
||||
}
|
||||
|
||||
const onClickOutside = (e) => {
|
||||
if (switcherRef.value && !switcherRef.value.contains(e.target)) {
|
||||
open.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', onClickOutside)
|
||||
document.documentElement.lang = locale.value
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', onClickOutside)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
/* Light theme (default - for white header backgrounds) */
|
||||
.switcher-trigger {
|
||||
background: transparent;
|
||||
color: #333;
|
||||
border: 1px solid #CCC;
|
||||
padding: 4px 12px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
transition: border-color 0.2s, opacity 0.2s;
|
||||
}
|
||||
|
||||
.switcher-trigger:hover {
|
||||
border-color: #999;
|
||||
}
|
||||
|
||||
.caret {
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
|
||||
.switcher-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
margin-top: 4px;
|
||||
background: #FFFFFF;
|
||||
border: 1px solid #DDD;
|
||||
list-style: none;
|
||||
padding: 4px 0;
|
||||
min-width: 100%;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.switcher-option {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.8rem;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
|
||||
.switcher-option:hover {
|
||||
background: #F0F0F0;
|
||||
}
|
||||
|
||||
.switcher-option.active {
|
||||
color: var(--orange, #FF4500);
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -6,25 +6,25 @@
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">01</span>
|
||||
<span class="step-title">本体生成</span>
|
||||
<span class="step-title">{{ $t('step1.ontologyGeneration') }}</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="currentPhase > 0" class="badge success">已完成</span>
|
||||
<span v-else-if="currentPhase === 0" class="badge processing">生成中</span>
|
||||
<span v-else class="badge pending">等待</span>
|
||||
<span v-if="currentPhase > 0" class="badge success">{{ $t('step1.ontologyCompleted') }}</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>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/graph/ontology/generate</p>
|
||||
<p class="description">
|
||||
LLM分析文档内容与模拟需求,提取出现实种子,自动生成合适的本体结构
|
||||
{{ $t('step1.ontologyDesc') }}
|
||||
</p>
|
||||
|
||||
<!-- Loading / Progress -->
|
||||
<div v-if="currentPhase === 0 && ontologyProgress" class="progress-section">
|
||||
<div class="spinner-sm"></div>
|
||||
<span>{{ ontologyProgress.message || '正在分析文档...' }}</span>
|
||||
<span>{{ ontologyProgress.message || $t('step1.analyzingDocs') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Detail Overlay -->
|
||||
@@ -110,34 +110,34 @@
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">02</span>
|
||||
<span class="step-title">GraphRAG构建</span>
|
||||
<span class="step-title">{{ $t('step1.graphRagBuild') }}</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="currentPhase > 1" class="badge success">已完成</span>
|
||||
<span v-if="currentPhase > 1" class="badge success">{{ $t('step1.ontologyCompleted') }}</span>
|
||||
<span v-else-if="currentPhase === 1" class="badge processing">{{ buildProgress?.progress || 0 }}%</span>
|
||||
<span v-else class="badge pending">等待</span>
|
||||
<span v-else class="badge pending">{{ $t('step1.ontologyPending') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/graph/build</p>
|
||||
<p class="description">
|
||||
基于生成的本体,将文档自动分块后调用 Zep 构建知识图谱,提取实体和关系,并形成时序记忆与社区摘要
|
||||
{{ $t('step1.graphRagDesc') }}
|
||||
</p>
|
||||
|
||||
<!-- Stats Cards -->
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ graphStats.nodes }}</span>
|
||||
<span class="stat-label">实体节点</span>
|
||||
<span class="stat-label">{{ $t('step1.entityNodes') }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ graphStats.edges }}</span>
|
||||
<span class="stat-label">关系边</span>
|
||||
<span class="stat-label">{{ $t('step1.relationEdges') }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ graphStats.types }}</span>
|
||||
<span class="stat-label">SCHEMA类型</span>
|
||||
<span class="stat-label">{{ $t('step1.schemaTypes') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,23 +148,23 @@
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">03</span>
|
||||
<span class="step-title">构建完成</span>
|
||||
<span class="step-title">{{ $t('step1.buildComplete') }}</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="currentPhase >= 2" class="badge accent">进行中</span>
|
||||
<span v-if="currentPhase >= 2" class="badge accent">{{ $t('step1.inProgress') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/simulation/create</p>
|
||||
<p class="description">图谱构建已完成,请进入下一步进行模拟环境搭建</p>
|
||||
<p class="description">{{ $t('step1.buildCompleteDesc') }}</p>
|
||||
<button
|
||||
class="action-btn"
|
||||
:disabled="currentPhase < 2 || creatingSimulation"
|
||||
@click="handleEnterEnvSetup"
|
||||
>
|
||||
<span v-if="creatingSimulation" class="spinner-sm"></span>
|
||||
{{ creatingSimulation ? '创建中...' : '进入环境搭建 ➝' }}
|
||||
{{ creatingSimulation ? $t('step1.creating') : $t('step1.enterEnvSetup') + ' ➝' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,9 +189,11 @@
|
||||
<script setup>
|
||||
import { computed, ref, watch, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { createSimulation } from '../api/simulation'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
currentPhase: { type: Number, default: 0 },
|
||||
@@ -233,11 +235,11 @@ const handleEnterEnvSetup = async () => {
|
||||
})
|
||||
} else {
|
||||
console.error('创建模拟失败:', res.error)
|
||||
alert('创建模拟失败: ' + (res.error || '未知错误'))
|
||||
alert(t('step1.createSimulationFailed', { error: res.error || t('common.unknownError') }))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('创建模拟异常:', err)
|
||||
alert('创建模拟异常: ' + err.message)
|
||||
alert(t('step1.createSimulationException', { error: err.message }))
|
||||
} finally {
|
||||
creatingSimulation.value = false
|
||||
}
|
||||
|
||||
@@ -6,18 +6,18 @@
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">01</span>
|
||||
<span class="step-title">模拟实例初始化</span>
|
||||
<span class="step-title">{{ $t('step2.simInstanceInit') }}</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="phase > 0" class="badge success">已完成</span>
|
||||
<span v-else class="badge processing">初始化</span>
|
||||
<span v-if="phase > 0" class="badge success">{{ $t('common.completed') }}</span>
|
||||
<span v-else class="badge processing">{{ $t('step2.initializing') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/simulation/create</p>
|
||||
<p class="description">
|
||||
新建simulation实例,拉取模拟世界参数模版
|
||||
{{ $t('step2.simInstanceDesc') }}
|
||||
</p>
|
||||
|
||||
<div v-if="simulationId" class="info-card">
|
||||
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="info-label">Task ID</span>
|
||||
<span class="info-value mono">{{ taskId || '异步任务已完成' }}</span>
|
||||
<span class="info-value mono">{{ taskId || $t('step2.asyncTaskDone') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -46,41 +46,41 @@
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">02</span>
|
||||
<span class="step-title">生成 Agent 人设</span>
|
||||
<span class="step-title">{{ $t('step2.generateAgentPersona') }}</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="phase > 1" class="badge success">已完成</span>
|
||||
<span v-if="phase > 1" class="badge success">{{ $t('common.completed') }}</span>
|
||||
<span v-else-if="phase === 1" class="badge processing">{{ prepareProgress }}%</span>
|
||||
<span v-else class="badge pending">等待</span>
|
||||
<span v-else class="badge pending">{{ $t('common.pending') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/simulation/prepare</p>
|
||||
<p class="description">
|
||||
结合上下文,自动调用工具从知识图谱梳理实体与关系,初始化模拟个体,并基于现实种子赋予他们独特的行为与记忆
|
||||
{{ $t('step2.generateAgentPersonaDesc') }}
|
||||
</p>
|
||||
|
||||
<!-- Profiles Stats -->
|
||||
<div v-if="profiles.length > 0" class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ profiles.length }}</span>
|
||||
<span class="stat-label">当前Agent数</span>
|
||||
<span class="stat-label">{{ $t('step2.currentAgentCount') }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ expectedTotal || '-' }}</span>
|
||||
<span class="stat-label">预期Agent总数</span>
|
||||
<span class="stat-label">{{ $t('step2.expectedAgentTotal') }}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ totalTopicsCount }}</span>
|
||||
<span class="stat-label">现实种子当前关联话题数</span>
|
||||
<span class="stat-label">{{ $t('step2.relatedTopicsCount') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Profiles List Preview -->
|
||||
<div v-if="profiles.length > 0" class="profiles-preview">
|
||||
<div class="preview-header">
|
||||
<span class="preview-title">已生成的 Agent 人设</span>
|
||||
<span class="preview-title">{{ $t('step2.generatedAgentPersonas') }}</span>
|
||||
</div>
|
||||
<div class="profiles-list">
|
||||
<div
|
||||
@@ -94,9 +94,9 @@
|
||||
<span class="profile-username">@{{ profile.name || `agent_${idx}` }}</span>
|
||||
</div>
|
||||
<div class="profile-meta">
|
||||
<span class="profile-profession">{{ profile.profession || '未知职业' }}</span>
|
||||
<span class="profile-profession">{{ profile.profession || $t('step2.unknownProfession') }}</span>
|
||||
</div>
|
||||
<p class="profile-bio">{{ profile.bio || '暂无简介' }}</p>
|
||||
<p class="profile-bio">{{ profile.bio || $t('step2.noBio') }}</p>
|
||||
<div v-if="profile.interested_topics?.length" class="profile-topics">
|
||||
<span
|
||||
v-for="topic in profile.interested_topics.slice(0, 3)"
|
||||
@@ -118,19 +118,19 @@
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">03</span>
|
||||
<span class="step-title">生成双平台模拟配置</span>
|
||||
<span class="step-title">{{ $t('step2.dualPlatformConfig') }}</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="phase > 2" class="badge success">已完成</span>
|
||||
<span v-else-if="phase === 2" class="badge processing">生成中</span>
|
||||
<span v-else class="badge pending">等待</span>
|
||||
<span v-if="phase > 2" class="badge success">{{ $t('common.completed') }}</span>
|
||||
<span v-else-if="phase === 2" class="badge processing">{{ $t('step2.generating') }}</span>
|
||||
<span v-else class="badge pending">{{ $t('common.pending') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/simulation/prepare</p>
|
||||
<p class="description">
|
||||
LLM 根据模拟需求与现实种子,智能设置世界时间流速、推荐算法、每个个体的活跃时间段、发言频率、事件触发等参数
|
||||
{{ $t('step2.dualPlatformConfigDesc') }}
|
||||
</p>
|
||||
|
||||
<!-- Config Preview -->
|
||||
@@ -139,40 +139,40 @@
|
||||
<div class="config-block">
|
||||
<div class="config-grid">
|
||||
<div class="config-item">
|
||||
<span class="config-item-label">模拟时长</span>
|
||||
<span class="config-item-value">{{ simulationConfig.time_config?.total_simulation_hours || '-' }} 小时</span>
|
||||
<span class="config-item-label">{{ $t('step2.simulationDuration') }}</span>
|
||||
<span class="config-item-value">{{ simulationConfig.time_config?.total_simulation_hours || '-' }} {{ $t('common.hours') }}</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-item-label">每轮时长</span>
|
||||
<span class="config-item-value">{{ simulationConfig.time_config?.minutes_per_round || '-' }} 分钟</span>
|
||||
<span class="config-item-label">{{ $t('step2.roundDuration') }}</span>
|
||||
<span class="config-item-value">{{ simulationConfig.time_config?.minutes_per_round || '-' }} {{ $t('common.minutes') }}</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-item-label">总轮次</span>
|
||||
<span class="config-item-value">{{ Math.floor((simulationConfig.time_config?.total_simulation_hours * 60 / simulationConfig.time_config?.minutes_per_round)) || '-' }} 轮</span>
|
||||
<span class="config-item-label">{{ $t('step2.totalRounds') }}</span>
|
||||
<span class="config-item-value">{{ Math.floor((simulationConfig.time_config?.total_simulation_hours * 60 / simulationConfig.time_config?.minutes_per_round)) || '-' }} {{ $t('common.rounds') }}</span>
|
||||
</div>
|
||||
<div class="config-item">
|
||||
<span class="config-item-label">每小时活跃</span>
|
||||
<span class="config-item-label">{{ $t('step2.activePerHour') }}</span>
|
||||
<span class="config-item-value">{{ simulationConfig.time_config?.agents_per_hour_min }}-{{ simulationConfig.time_config?.agents_per_hour_max }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="time-periods">
|
||||
<div class="period-item">
|
||||
<span class="period-label">高峰时段</span>
|
||||
<span class="period-label">{{ $t('step2.peakHours') }}</span>
|
||||
<span class="period-hours">{{ simulationConfig.time_config?.peak_hours?.join(':00, ') }}:00</span>
|
||||
<span class="period-multiplier">×{{ simulationConfig.time_config?.peak_activity_multiplier }}</span>
|
||||
</div>
|
||||
<div class="period-item">
|
||||
<span class="period-label">工作时段</span>
|
||||
<span class="period-label">{{ $t('step2.workHours') }}</span>
|
||||
<span class="period-hours">{{ simulationConfig.time_config?.work_hours?.[0] }}:00-{{ simulationConfig.time_config?.work_hours?.slice(-1)[0] }}:00</span>
|
||||
<span class="period-multiplier">×{{ simulationConfig.time_config?.work_activity_multiplier }}</span>
|
||||
</div>
|
||||
<div class="period-item">
|
||||
<span class="period-label">早间时段</span>
|
||||
<span class="period-label">{{ $t('step2.morningHours') }}</span>
|
||||
<span class="period-hours">{{ simulationConfig.time_config?.morning_hours?.[0] }}:00-{{ simulationConfig.time_config?.morning_hours?.slice(-1)[0] }}:00</span>
|
||||
<span class="period-multiplier">×{{ simulationConfig.time_config?.morning_activity_multiplier }}</span>
|
||||
</div>
|
||||
<div class="period-item">
|
||||
<span class="period-label">低谷时段</span>
|
||||
<span class="period-label">{{ $t('step2.offPeakHours') }}</span>
|
||||
<span class="period-hours">{{ simulationConfig.time_config?.off_peak_hours?.[0] }}:00-{{ simulationConfig.time_config?.off_peak_hours?.slice(-1)[0] }}:00</span>
|
||||
<span class="period-multiplier">×{{ simulationConfig.time_config?.off_peak_activity_multiplier }}</span>
|
||||
</div>
|
||||
@@ -182,8 +182,8 @@
|
||||
<!-- Agent 配置 -->
|
||||
<div class="config-block">
|
||||
<div class="config-block-header">
|
||||
<span class="config-block-title">Agent 配置</span>
|
||||
<span class="config-block-badge">{{ simulationConfig.agent_configs?.length || 0 }} 个</span>
|
||||
<span class="config-block-title">{{ $t('step2.agentConfig') }}</span>
|
||||
<span class="config-block-badge">{{ simulationConfig.agent_configs?.length || 0 }} {{ $t('common.items') }}</span>
|
||||
</div>
|
||||
<div class="agents-cards">
|
||||
<div
|
||||
@@ -205,7 +205,7 @@
|
||||
|
||||
<!-- 活跃时间轴 -->
|
||||
<div class="agent-timeline">
|
||||
<span class="timeline-label">活跃时段</span>
|
||||
<span class="timeline-label">{{ $t('step2.activeTimePeriod') }}</span>
|
||||
<div class="mini-timeline">
|
||||
<div
|
||||
v-for="hour in 24"
|
||||
@@ -228,34 +228,34 @@
|
||||
<div class="agent-params">
|
||||
<div class="param-group">
|
||||
<div class="param-item">
|
||||
<span class="param-label">发帖/时</span>
|
||||
<span class="param-label">{{ $t('step2.postsPerHour') }}</span>
|
||||
<span class="param-value">{{ agent.posts_per_hour }}</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-label">评论/时</span>
|
||||
<span class="param-label">{{ $t('step2.commentsPerHour') }}</span>
|
||||
<span class="param-value">{{ agent.comments_per_hour }}</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-label">响应延迟</span>
|
||||
<span class="param-label">{{ $t('step2.responseDelay') }}</span>
|
||||
<span class="param-value">{{ agent.response_delay_min }}-{{ agent.response_delay_max }}min</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="param-group">
|
||||
<div class="param-item">
|
||||
<span class="param-label">活跃度</span>
|
||||
<span class="param-label">{{ $t('step2.activityLevel') }}</span>
|
||||
<span class="param-value with-bar">
|
||||
<span class="mini-bar" :style="{ width: (agent.activity_level * 100) + '%' }"></span>
|
||||
{{ (agent.activity_level * 100).toFixed(0) }}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-label">情感倾向</span>
|
||||
<span class="param-label">{{ $t('step2.sentimentBias') }}</span>
|
||||
<span class="param-value" :class="agent.sentiment_bias > 0 ? 'positive' : agent.sentiment_bias < 0 ? 'negative' : 'neutral'">
|
||||
{{ agent.sentiment_bias > 0 ? '+' : '' }}{{ agent.sentiment_bias?.toFixed(1) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="param-item">
|
||||
<span class="param-label">影响力</span>
|
||||
<span class="param-label">{{ $t('step2.influenceWeight') }}</span>
|
||||
<span class="param-value highlight">{{ agent.influence_weight?.toFixed(1) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -267,59 +267,59 @@
|
||||
<!-- 平台配置 -->
|
||||
<div class="config-block">
|
||||
<div class="config-block-header">
|
||||
<span class="config-block-title">推荐算法配置</span>
|
||||
<span class="config-block-title">{{ $t('step2.recommendAlgoConfig') }}</span>
|
||||
</div>
|
||||
<div class="platforms-grid">
|
||||
<div v-if="simulationConfig.twitter_config" class="platform-card">
|
||||
<div class="platform-card-header">
|
||||
<span class="platform-name">平台 1:广场 / 信息流</span>
|
||||
<span class="platform-name">{{ $t('step2.platform1Name') }}</span>
|
||||
</div>
|
||||
<div class="platform-params">
|
||||
<div class="param-row">
|
||||
<span class="param-label">时效权重</span>
|
||||
<span class="param-label">{{ $t('step2.recencyWeight') }}</span>
|
||||
<span class="param-value">{{ simulationConfig.twitter_config.recency_weight }}</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">热度权重</span>
|
||||
<span class="param-label">{{ $t('step2.popularityWeight') }}</span>
|
||||
<span class="param-value">{{ simulationConfig.twitter_config.popularity_weight }}</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">相关性权重</span>
|
||||
<span class="param-label">{{ $t('step2.relevanceWeight') }}</span>
|
||||
<span class="param-value">{{ simulationConfig.twitter_config.relevance_weight }}</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">病毒阈值</span>
|
||||
<span class="param-label">{{ $t('step2.viralThreshold') }}</span>
|
||||
<span class="param-value">{{ simulationConfig.twitter_config.viral_threshold }}</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">回音室强度</span>
|
||||
<span class="param-label">{{ $t('step2.echoChamberStrength') }}</span>
|
||||
<span class="param-value">{{ simulationConfig.twitter_config.echo_chamber_strength }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="simulationConfig.reddit_config" class="platform-card">
|
||||
<div class="platform-card-header">
|
||||
<span class="platform-name">平台 2:话题 / 社区</span>
|
||||
<span class="platform-name">{{ $t('step2.platform2Name') }}</span>
|
||||
</div>
|
||||
<div class="platform-params">
|
||||
<div class="param-row">
|
||||
<span class="param-label">时效权重</span>
|
||||
<span class="param-label">{{ $t('step2.recencyWeight') }}</span>
|
||||
<span class="param-value">{{ simulationConfig.reddit_config.recency_weight }}</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">热度权重</span>
|
||||
<span class="param-label">{{ $t('step2.popularityWeight') }}</span>
|
||||
<span class="param-value">{{ simulationConfig.reddit_config.popularity_weight }}</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">相关性权重</span>
|
||||
<span class="param-label">{{ $t('step2.relevanceWeight') }}</span>
|
||||
<span class="param-value">{{ simulationConfig.reddit_config.relevance_weight }}</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">病毒阈值</span>
|
||||
<span class="param-label">{{ $t('step2.viralThreshold') }}</span>
|
||||
<span class="param-value">{{ simulationConfig.reddit_config.viral_threshold }}</span>
|
||||
</div>
|
||||
<div class="param-row">
|
||||
<span class="param-label">回音室强度</span>
|
||||
<span class="param-label">{{ $t('step2.echoChamberStrength') }}</span>
|
||||
<span class="param-value">{{ simulationConfig.reddit_config.echo_chamber_strength }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -330,7 +330,7 @@
|
||||
<!-- LLM 配置推理 -->
|
||||
<div v-if="simulationConfig.generation_reasoning" class="config-block">
|
||||
<div class="config-block-header">
|
||||
<span class="config-block-title">LLM 配置推理</span>
|
||||
<span class="config-block-title">{{ $t('step2.llmConfigReasoning') }}</span>
|
||||
</div>
|
||||
<div class="reasoning-content">
|
||||
<div
|
||||
@@ -351,19 +351,19 @@
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">04</span>
|
||||
<span class="step-title">初始激活编排</span>
|
||||
<span class="step-title">{{ $t('step2.initialActivation') }}</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="phase > 3" class="badge success">已完成</span>
|
||||
<span v-else-if="phase === 3" class="badge processing">编排中</span>
|
||||
<span v-else class="badge pending">等待</span>
|
||||
<span v-if="phase > 3" class="badge success">{{ $t('common.completed') }}</span>
|
||||
<span v-else-if="phase === 3" class="badge processing">{{ $t('step2.orchestrating') }}</span>
|
||||
<span v-else class="badge pending">{{ $t('common.pending') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/simulation/prepare</p>
|
||||
<p class="description">
|
||||
基于叙事方向,自动生成初始激活事件与热点话题,引导模拟世界的初始状态
|
||||
{{ $t('step2.initialActivationDesc') }}
|
||||
</p>
|
||||
|
||||
<div v-if="simulationConfig?.event_config" class="orchestration-content">
|
||||
@@ -380,14 +380,14 @@
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
叙事引导方向
|
||||
{{ $t('step2.narrativeDirection') }}
|
||||
</span>
|
||||
<p class="narrative-text">{{ simulationConfig.event_config.narrative_direction }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 热点话题 -->
|
||||
<div class="topics-section">
|
||||
<span class="box-label">初始热点话题</span>
|
||||
<span class="box-label">{{ $t('step2.initialHotTopics') }}</span>
|
||||
<div class="hot-topics-grid">
|
||||
<span v-for="topic in simulationConfig.event_config.hot_topics" :key="topic" class="hot-topic-tag">
|
||||
# {{ topic }}
|
||||
@@ -397,7 +397,7 @@
|
||||
|
||||
<!-- 初始帖子流 -->
|
||||
<div class="initial-posts-section">
|
||||
<span class="box-label">初始激活序列 ({{ simulationConfig.event_config.initial_posts.length }})</span>
|
||||
<span class="box-label">{{ $t('step2.initialActivationSeq', { count: simulationConfig.event_config.initial_posts.length }) }}</span>
|
||||
<div class="posts-timeline">
|
||||
<div v-for="(post, idx) in simulationConfig.event_config.initial_posts" :key="idx" class="timeline-item">
|
||||
<div class="timeline-marker"></div>
|
||||
@@ -423,29 +423,29 @@
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">05</span>
|
||||
<span class="step-title">准备完成</span>
|
||||
<span class="step-title">{{ $t('step2.setupComplete') }}</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="phase >= 4" class="badge processing">进行中</span>
|
||||
<span v-else class="badge pending">等待</span>
|
||||
<span v-if="phase >= 4" class="badge processing">{{ $t('step1.inProgress') }}</span>
|
||||
<span v-else class="badge pending">{{ $t('common.pending') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/simulation/start</p>
|
||||
<p class="description">模拟环境已准备完成,可以开始运行模拟</p>
|
||||
<p class="description">{{ $t('step2.setupCompleteDesc') }}</p>
|
||||
|
||||
<!-- 模拟轮数配置 - 只有在配置生成完成且轮数计算出来后才显示 -->
|
||||
<div v-if="simulationConfig && autoGeneratedRounds" class="rounds-config-section">
|
||||
<div class="rounds-header">
|
||||
<div class="header-left">
|
||||
<span class="section-title">模拟轮数设定</span>
|
||||
<span class="section-desc">MiroFish 自动规划推演现实 <span class="desc-highlight">{{ simulationConfig?.time_config?.total_simulation_hours || '-' }}</span> 小时,每轮代表现实 <span class="desc-highlight">{{ simulationConfig?.time_config?.minutes_per_round || '-' }}</span> 分钟时间流逝</span>
|
||||
<span class="section-title">{{ $t('step2.roundsConfig') }}</span>
|
||||
<span class="section-desc">{{ $t('step2.roundsConfigDesc', { hours: simulationConfig?.time_config?.total_simulation_hours || '-', minutesPerRound: simulationConfig?.time_config?.minutes_per_round || '-' }) }}</span>
|
||||
</div>
|
||||
<label class="switch-control">
|
||||
<input type="checkbox" v-model="useCustomRounds">
|
||||
<span class="switch-track"></span>
|
||||
<span class="switch-label">自定义</span>
|
||||
<span class="switch-label">{{ $t('step2.customToggle') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -454,10 +454,10 @@
|
||||
<div class="slider-display">
|
||||
<div class="slider-main-value">
|
||||
<span class="val-num">{{ customMaxRounds }}</span>
|
||||
<span class="val-unit">轮</span>
|
||||
<span class="val-unit">{{ $t('step2.roundsUnit') }}</span>
|
||||
</div>
|
||||
<div class="slider-meta-info">
|
||||
<span>若Agent规模为100:预计耗时约 {{ Math.round(customMaxRounds * 0.6) }} 分钟</span>
|
||||
<span>{{ $t('step2.estimatedDuration', { minutes: Math.round(customMaxRounds * 0.6) }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -478,7 +478,7 @@
|
||||
:class="{ active: customMaxRounds === 40 }"
|
||||
@click="customMaxRounds = 40"
|
||||
:style="{ position: 'absolute', left: `calc(${(40 - 10) / (autoGeneratedRounds - 10) * 100}% - 30px)` }"
|
||||
>40 (推荐)</span>
|
||||
>{{ $t('step2.recommendedRounds', { rounds: 40 }) }}</span>
|
||||
<span>{{ autoGeneratedRounds }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -488,7 +488,7 @@
|
||||
<div class="auto-info-card">
|
||||
<div class="auto-value">
|
||||
<span class="val-num">{{ autoGeneratedRounds }}</span>
|
||||
<span class="val-unit">轮</span>
|
||||
<span class="val-unit">{{ $t('step2.roundsUnit') }}</span>
|
||||
</div>
|
||||
<div class="auto-content">
|
||||
<div class="auto-meta-row">
|
||||
@@ -497,11 +497,11 @@
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<polyline points="12 6 12 12 16 14"></polyline>
|
||||
</svg>
|
||||
若Agent规模为100:预计耗时 {{ Math.round(autoGeneratedRounds * 0.6) }} 分钟
|
||||
{{ $t('step2.estimatedDurationFull', { minutes: Math.round(autoGeneratedRounds * 0.6) }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="auto-desc">
|
||||
<p class="highlight-tip" @click="useCustomRounds = true">若首次运行,强烈建议切换至‘自定义模式’减少模拟轮数,以便快速预览效果并降低报错风险 ➝</p>
|
||||
<p class="highlight-tip" @click="useCustomRounds = true">{{ $t('step2.customTip') }} ➝</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -514,14 +514,14 @@
|
||||
class="action-btn secondary"
|
||||
@click="$emit('go-back')"
|
||||
>
|
||||
← 返回图谱构建
|
||||
← {{ $t('step2.backToGraphBuild') }}
|
||||
</button>
|
||||
<button
|
||||
class="action-btn primary"
|
||||
:disabled="phase < 4"
|
||||
@click="handleStartSimulation"
|
||||
>
|
||||
开始双世界并行模拟 ➝
|
||||
{{ $t('step2.startDualWorldSim') }} ➝
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -547,32 +547,32 @@
|
||||
<!-- 基本信息 -->
|
||||
<div class="modal-info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">事件外显年龄</span>
|
||||
<span class="info-value">{{ selectedProfile.age || '-' }} 岁</span>
|
||||
<span class="info-label">{{ $t('step2.profileModalAge') }}</span>
|
||||
<span class="info-value">{{ selectedProfile.age || '-' }} {{ $t('step2.yearsOld') }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">事件外显性别</span>
|
||||
<span class="info-value">{{ { male: '男', female: '女', other: '其他' }[selectedProfile.gender] || selectedProfile.gender }}</span>
|
||||
<span class="info-label">{{ $t('step2.profileModalGender') }}</span>
|
||||
<span class="info-value">{{ { male: $t('step2.genderMale'), female: $t('step2.genderFemale'), other: $t('step2.genderOther') }[selectedProfile.gender] || selectedProfile.gender }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">国家/地区</span>
|
||||
<span class="info-label">{{ $t('step2.profileModalCountry') }}</span>
|
||||
<span class="info-value">{{ selectedProfile.country || '-' }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">事件外显MBTI</span>
|
||||
<span class="info-label">{{ $t('step2.profileModalMbti') }}</span>
|
||||
<span class="info-value mbti">{{ selectedProfile.mbti || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 简介 -->
|
||||
<div class="modal-section">
|
||||
<span class="section-label">人设简介</span>
|
||||
<p class="section-bio">{{ selectedProfile.bio || '暂无简介' }}</p>
|
||||
<span class="section-label">{{ $t('step2.profileModalBio') }}</span>
|
||||
<p class="section-bio">{{ selectedProfile.bio || $t('step2.noBio') }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 关注话题 -->
|
||||
<div class="modal-section" v-if="selectedProfile.interested_topics?.length">
|
||||
<span class="section-label">现实种子关联话题</span>
|
||||
<span class="section-label">{{ $t('step2.profileModalTopics') }}</span>
|
||||
<div class="topics-grid">
|
||||
<span
|
||||
v-for="topic in selectedProfile.interested_topics"
|
||||
@@ -584,25 +584,25 @@
|
||||
|
||||
<!-- 详细人设 -->
|
||||
<div class="modal-section" v-if="selectedProfile.persona">
|
||||
<span class="section-label">详细人设背景</span>
|
||||
<span class="section-label">{{ $t('step2.profileModalPersona') }}</span>
|
||||
|
||||
<!-- 人设维度概览 -->
|
||||
<div class="persona-dimensions">
|
||||
<div class="dimension-card">
|
||||
<span class="dim-title">事件全景经历</span>
|
||||
<span class="dim-desc">在此事件中的完整行为轨迹</span>
|
||||
<span class="dim-title">{{ $t('step2.personaDimExperience') }}</span>
|
||||
<span class="dim-desc">{{ $t('step2.personaDimExperienceDesc') }}</span>
|
||||
</div>
|
||||
<div class="dimension-card">
|
||||
<span class="dim-title">行为模式侧写</span>
|
||||
<span class="dim-desc">经验总结与行事风格偏好</span>
|
||||
<span class="dim-title">{{ $t('step2.personaDimBehavior') }}</span>
|
||||
<span class="dim-desc">{{ $t('step2.personaDimBehaviorDesc') }}</span>
|
||||
</div>
|
||||
<div class="dimension-card">
|
||||
<span class="dim-title">独特记忆印记</span>
|
||||
<span class="dim-desc">基于现实种子形成的记忆</span>
|
||||
<span class="dim-title">{{ $t('step2.personaDimMemory') }}</span>
|
||||
<span class="dim-desc">{{ $t('step2.personaDimMemoryDesc') }}</span>
|
||||
</div>
|
||||
<div class="dimension-card">
|
||||
<span class="dim-title">社会关系网络</span>
|
||||
<span class="dim-desc">个体链接与交互图谱</span>
|
||||
<span class="dim-title">{{ $t('step2.personaDimSocial') }}</span>
|
||||
<span class="dim-desc">{{ $t('step2.personaDimSocialDesc') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -633,14 +633,17 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import {
|
||||
prepareSimulation,
|
||||
getPrepareStatus,
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
prepareSimulation,
|
||||
getPrepareStatus,
|
||||
getSimulationProfilesRealtime,
|
||||
getSimulationConfig,
|
||||
getSimulationConfigRealtime
|
||||
getSimulationConfigRealtime
|
||||
} from '../api/simulation'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
simulationId: String, // 从父组件传入
|
||||
projectData: Object,
|
||||
@@ -680,7 +683,7 @@ watch(currentStage, (newStage) => {
|
||||
phase.value = 2
|
||||
// 进入配置生成阶段,开始轮询配置
|
||||
if (!configTimer) {
|
||||
addLog('开始生成双平台模拟配置...')
|
||||
addLog(t('log.startGeneratingConfig'))
|
||||
startConfigPolling()
|
||||
}
|
||||
} else if (newStage === '准备模拟脚本' || newStage === 'copying_scripts') {
|
||||
@@ -745,10 +748,10 @@ const handleStartSimulation = () => {
|
||||
if (useCustomRounds.value) {
|
||||
// 用户自定义轮数,传递 max_rounds 参数
|
||||
params.maxRounds = customMaxRounds.value
|
||||
addLog(`开始模拟,自定义轮数: ${customMaxRounds.value} 轮`)
|
||||
addLog(t('log.startSimCustomRounds', { rounds: customMaxRounds.value }))
|
||||
} else {
|
||||
// 用户选择保持自动生成的轮数,不传递 max_rounds 参数
|
||||
addLog(`开始模拟,使用自动配置轮数: ${autoGeneratedRounds.value} 轮`)
|
||||
addLog(t('log.startSimAutoRounds', { rounds: autoGeneratedRounds.value }))
|
||||
}
|
||||
|
||||
emit('next-step', params)
|
||||
@@ -768,15 +771,15 @@ const selectProfile = (profile) => {
|
||||
// 自动开始准备模拟
|
||||
const startPrepareSimulation = async () => {
|
||||
if (!props.simulationId) {
|
||||
addLog('错误:缺少 simulationId')
|
||||
addLog(t('log.errorMissingSimId'))
|
||||
emit('update-status', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
// 标记第一步完成,开始第二步
|
||||
phase.value = 1
|
||||
addLog(`模拟实例已创建: ${props.simulationId}`)
|
||||
addLog('正在准备模拟环境...')
|
||||
addLog(t('log.simInstanceCreated', { id: props.simulationId }))
|
||||
addLog(t('log.preparingSimEnv'))
|
||||
emit('update-status', 'processing')
|
||||
|
||||
try {
|
||||
@@ -788,35 +791,35 @@ const startPrepareSimulation = async () => {
|
||||
|
||||
if (res.success && res.data) {
|
||||
if (res.data.already_prepared) {
|
||||
addLog('检测到已有完成的准备工作,直接使用')
|
||||
addLog(t('log.detectedExistingPrep'))
|
||||
await loadPreparedData()
|
||||
return
|
||||
}
|
||||
|
||||
taskId.value = res.data.task_id
|
||||
addLog(`准备任务已启动`)
|
||||
addLog(` └─ Task ID: ${res.data.task_id}`)
|
||||
addLog(t('log.prepareTaskStarted'))
|
||||
addLog(t('log.prepareTaskId', { taskId: res.data.task_id }))
|
||||
|
||||
// 立即设置预期Agent总数(从prepare接口返回值获取)
|
||||
if (res.data.expected_entities_count) {
|
||||
expectedTotal.value = res.data.expected_entities_count
|
||||
addLog(`从Zep图谱读取到 ${res.data.expected_entities_count} 个实体`)
|
||||
addLog(t('log.zepEntitiesFound', { count: res.data.expected_entities_count }))
|
||||
if (res.data.entity_types && res.data.entity_types.length > 0) {
|
||||
addLog(` └─ 实体类型: ${res.data.entity_types.join(', ')}`)
|
||||
addLog(t('log.entityTypes', { types: res.data.entity_types.join(', ') }))
|
||||
}
|
||||
}
|
||||
|
||||
addLog('开始轮询准备进度...')
|
||||
addLog(t('log.startPollingProgress'))
|
||||
// 开始轮询进度
|
||||
startPolling()
|
||||
// 开始实时获取 Profiles
|
||||
startProfilesPolling()
|
||||
} else {
|
||||
addLog(`准备失败: ${res.error || '未知错误'}`)
|
||||
addLog(t('log.prepareFailed', { error: res.error || t('common.unknownError') }))
|
||||
emit('update-status', 'error')
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`准备异常: ${err.message}`)
|
||||
addLog(t('log.prepareException', { error: err.message }))
|
||||
emit('update-status', 'error')
|
||||
}
|
||||
}
|
||||
@@ -890,12 +893,12 @@ const pollPrepareStatus = async () => {
|
||||
|
||||
// 检查是否完成
|
||||
if (data.status === 'completed' || data.status === 'ready' || data.already_prepared) {
|
||||
addLog('✓ 准备工作已完成')
|
||||
addLog(t('log.prepareComplete'))
|
||||
stopPolling()
|
||||
stopProfilesPolling()
|
||||
await loadPreparedData()
|
||||
} else if (data.status === 'failed') {
|
||||
addLog(`✗ 准备失败: ${data.error || '未知错误'}`)
|
||||
addLog(t('log.prepareFailedWithError', { error: data.error || t('common.unknownError') }))
|
||||
stopPolling()
|
||||
stopProfilesPolling()
|
||||
}
|
||||
@@ -934,13 +937,13 @@ const fetchProfilesRealtime = async () => {
|
||||
const latestProfile = profiles.value[currentCount - 1]
|
||||
const profileName = latestProfile?.name || latestProfile?.username || `Agent_${currentCount}`
|
||||
if (currentCount === 1) {
|
||||
addLog(`开始生成Agent人设...`)
|
||||
addLog(t('log.startGeneratingAgentProfiles'))
|
||||
}
|
||||
addLog(`→ Agent人设 ${currentCount}/${total}: ${profileName} (${latestProfile?.profession || '未知职业'})`)
|
||||
|
||||
addLog(t('log.agentProfile', { current: currentCount, total: total, name: profileName, profession: latestProfile?.profession || t('step2.unknownProfession') }))
|
||||
|
||||
// 如果全部生成完成
|
||||
if (expectedTotal.value && currentCount >= expectedTotal.value) {
|
||||
addLog(`✓ 全部 ${currentCount} 个Agent人设生成完成`)
|
||||
addLog(t('log.allProfilesComplete', { count: currentCount }))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -974,41 +977,41 @@ const fetchConfigRealtime = async () => {
|
||||
if (data.generation_stage && data.generation_stage !== lastLoggedConfigStage) {
|
||||
lastLoggedConfigStage = data.generation_stage
|
||||
if (data.generation_stage === 'generating_profiles') {
|
||||
addLog('正在生成Agent人设配置...')
|
||||
addLog(t('log.generatingAgentProfileConfig'))
|
||||
} else if (data.generation_stage === 'generating_config') {
|
||||
addLog('正在调用LLM生成模拟配置参数...')
|
||||
addLog(t('log.generatingLLMConfig'))
|
||||
}
|
||||
}
|
||||
|
||||
// 如果配置已生成
|
||||
if (data.config_generated && data.config) {
|
||||
simulationConfig.value = data.config
|
||||
addLog('✓ 模拟配置生成完成')
|
||||
|
||||
addLog(t('log.configComplete'))
|
||||
|
||||
// 显示详细配置摘要
|
||||
if (data.summary) {
|
||||
addLog(` ├─ Agent数量: ${data.summary.total_agents}个`)
|
||||
addLog(` ├─ 模拟时长: ${data.summary.simulation_hours}小时`)
|
||||
addLog(` ├─ 初始帖子: ${data.summary.initial_posts_count}条`)
|
||||
addLog(` ├─ 热点话题: ${data.summary.hot_topics_count}个`)
|
||||
addLog(` └─ 平台配置: Twitter ${data.summary.has_twitter_config ? '✓' : '✗'}, Reddit ${data.summary.has_reddit_config ? '✓' : '✗'}`)
|
||||
addLog(t('log.configSummaryAgents', { count: data.summary.total_agents }))
|
||||
addLog(t('log.configSummaryHours', { hours: data.summary.simulation_hours }))
|
||||
addLog(t('log.configSummaryPosts', { count: data.summary.initial_posts_count }))
|
||||
addLog(t('log.configSummaryTopics', { count: data.summary.hot_topics_count }))
|
||||
addLog(t('log.configSummaryPlatforms', { twitter: data.summary.has_twitter_config ? '✓' : '✗', reddit: data.summary.has_reddit_config ? '✓' : '✗' }))
|
||||
}
|
||||
|
||||
// 显示时间配置详情
|
||||
if (data.config.time_config) {
|
||||
const tc = data.config.time_config
|
||||
addLog(`时间配置: 每轮${tc.minutes_per_round}分钟, 共${Math.floor((tc.total_simulation_hours * 60) / tc.minutes_per_round)}轮`)
|
||||
addLog(t('log.timeConfigDetail', { minutes: tc.minutes_per_round, rounds: Math.floor((tc.total_simulation_hours * 60) / tc.minutes_per_round) }))
|
||||
}
|
||||
|
||||
// 显示事件配置
|
||||
if (data.config.event_config?.narrative_direction) {
|
||||
const narrative = data.config.event_config.narrative_direction
|
||||
addLog(`叙事方向: ${narrative.length > 50 ? narrative.substring(0, 50) + '...' : narrative}`)
|
||||
addLog(t('log.narrativeDirection', { direction: narrative.length > 50 ? narrative.substring(0, 50) + '...' : narrative }))
|
||||
}
|
||||
|
||||
stopConfigPolling()
|
||||
phase.value = 4
|
||||
addLog('✓ 环境搭建完成,可以开始模拟')
|
||||
addLog(t('log.envSetupComplete'))
|
||||
emit('update-status', 'completed')
|
||||
}
|
||||
}
|
||||
@@ -1019,11 +1022,11 @@ const fetchConfigRealtime = async () => {
|
||||
|
||||
const loadPreparedData = async () => {
|
||||
phase.value = 2
|
||||
addLog('正在加载已有配置数据...')
|
||||
addLog(t('log.loadingExistingConfig'))
|
||||
|
||||
// 最后获取一次 Profiles
|
||||
await fetchProfilesRealtime()
|
||||
addLog(`已加载 ${profiles.value.length} 个Agent人设`)
|
||||
addLog(t('log.loadedAgentProfiles', { count: profiles.value.length }))
|
||||
|
||||
// 获取配置(使用实时接口)
|
||||
try {
|
||||
@@ -1031,26 +1034,26 @@ const loadPreparedData = async () => {
|
||||
if (res.success && res.data) {
|
||||
if (res.data.config_generated && res.data.config) {
|
||||
simulationConfig.value = res.data.config
|
||||
addLog('✓ 模拟配置加载成功')
|
||||
|
||||
addLog(t('log.configLoadSuccess'))
|
||||
|
||||
// 显示详细配置摘要
|
||||
if (res.data.summary) {
|
||||
addLog(` ├─ Agent数量: ${res.data.summary.total_agents}个`)
|
||||
addLog(` ├─ 模拟时长: ${res.data.summary.simulation_hours}小时`)
|
||||
addLog(` └─ 初始帖子: ${res.data.summary.initial_posts_count}条`)
|
||||
addLog(t('log.configSummaryAgents', { count: res.data.summary.total_agents }))
|
||||
addLog(t('log.configSummaryHours', { hours: res.data.summary.simulation_hours }))
|
||||
addLog(t('log.configSummaryPostsAlt', { count: res.data.summary.initial_posts_count }))
|
||||
}
|
||||
|
||||
addLog('✓ 环境搭建完成,可以开始模拟')
|
||||
|
||||
addLog(t('log.envSetupComplete'))
|
||||
phase.value = 4
|
||||
emit('update-status', 'completed')
|
||||
} else {
|
||||
// 配置尚未生成,开始轮询
|
||||
addLog('配置生成中,开始轮询等待...')
|
||||
addLog(t('log.configGenerating'))
|
||||
startConfigPolling()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载配置失败: ${err.message}`)
|
||||
addLog(t('log.loadConfigFailed', { error: err.message }))
|
||||
emit('update-status', 'error')
|
||||
}
|
||||
}
|
||||
@@ -1068,7 +1071,7 @@ watch(() => props.systemLogs?.length, () => {
|
||||
onMounted(() => {
|
||||
// 自动开始准备流程
|
||||
if (props.simulationId) {
|
||||
addLog('Step2 环境搭建初始化')
|
||||
addLog(t('log.step2Init'))
|
||||
startPrepareSimulation()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<span class="stat-value mono">{{ runStatus.twitter_current_round || 0 }}<span class="stat-total">/{{ runStatus.total_rounds || maxRounds || '-' }}</span></span>
|
||||
</span>
|
||||
<span class="stat">
|
||||
<span class="stat-label">Elapsed Time</span>
|
||||
<span class="stat-label">TIME</span>
|
||||
<span class="stat-value mono">{{ twitterElapsedTime }}</span>
|
||||
</span>
|
||||
<span class="stat">
|
||||
@@ -63,7 +63,7 @@
|
||||
<span class="stat-value mono">{{ runStatus.reddit_current_round || 0 }}<span class="stat-total">/{{ runStatus.total_rounds || maxRounds || '-' }}</span></span>
|
||||
</span>
|
||||
<span class="stat">
|
||||
<span class="stat-label">Elapsed Time</span>
|
||||
<span class="stat-label">TIME</span>
|
||||
<span class="stat-value mono">{{ redditElapsedTime }}</span>
|
||||
</span>
|
||||
<span class="stat">
|
||||
@@ -97,7 +97,7 @@
|
||||
@click="handleNextStep"
|
||||
>
|
||||
<span v-if="isGeneratingReport" class="loading-spinner-small"></span>
|
||||
{{ isGeneratingReport ? '启动中...' : '开始生成结果报告' }}
|
||||
{{ isGeneratingReport ? $t('step3.generatingReportBtn') : $t('step3.startGenerateReportBtn') }}
|
||||
<span v-if="!isGeneratingReport" class="arrow-icon">→</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -288,14 +288,17 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import {
|
||||
startSimulation,
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
startSimulation,
|
||||
stopSimulation,
|
||||
getRunStatus,
|
||||
getRunStatus,
|
||||
getRunStatusDetail
|
||||
} from '../api/simulation'
|
||||
import { generateReport } from '../api/report'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
simulationId: String,
|
||||
maxRounds: Number, // 从Step2传入的最大轮数
|
||||
@@ -379,16 +382,16 @@ const resetAllState = () => {
|
||||
// 启动模拟
|
||||
const doStartSimulation = async () => {
|
||||
if (!props.simulationId) {
|
||||
addLog('错误:缺少 simulationId')
|
||||
addLog(t('log.errorMissingSimId'))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 先重置所有状态,确保不会受到上一次模拟的影响
|
||||
resetAllState()
|
||||
|
||||
isStarting.value = true
|
||||
startError.value = null
|
||||
addLog('正在启动双平台并行模拟...')
|
||||
addLog(t('log.startingDualSim'))
|
||||
emit('update-status', 'processing')
|
||||
|
||||
try {
|
||||
@@ -401,18 +404,18 @@ const doStartSimulation = async () => {
|
||||
|
||||
if (props.maxRounds) {
|
||||
params.max_rounds = props.maxRounds
|
||||
addLog(`设置最大模拟轮数: ${props.maxRounds}`)
|
||||
addLog(t('log.setMaxRounds', { rounds: props.maxRounds }))
|
||||
}
|
||||
|
||||
addLog('已开启动态图谱更新模式')
|
||||
addLog(t('log.graphMemoryUpdateEnabled'))
|
||||
|
||||
const res = await startSimulation(params)
|
||||
|
||||
if (res.success && res.data) {
|
||||
if (res.data.force_restarted) {
|
||||
addLog('✓ 已清理旧的模拟日志,重新开始模拟')
|
||||
addLog(t('log.oldSimCleared'))
|
||||
}
|
||||
addLog('✓ 模拟引擎启动成功')
|
||||
addLog(t('log.engineStarted'))
|
||||
addLog(` ├─ PID: ${res.data.process_pid || '-'}`)
|
||||
|
||||
phase.value = 1
|
||||
@@ -422,12 +425,12 @@ const doStartSimulation = async () => {
|
||||
startDetailPolling()
|
||||
} else {
|
||||
startError.value = res.error || '启动失败'
|
||||
addLog(`✗ 启动失败: ${res.error || '未知错误'}`)
|
||||
addLog(t('log.startFailed', { error: res.error || t('common.unknownError') }))
|
||||
emit('update-status', 'error')
|
||||
}
|
||||
} catch (err) {
|
||||
startError.value = err.message
|
||||
addLog(`✗ 启动异常: ${err.message}`)
|
||||
addLog(t('log.startException', { error: err.message }))
|
||||
emit('update-status', 'error')
|
||||
} finally {
|
||||
isStarting.value = false
|
||||
@@ -439,21 +442,21 @@ const handleStopSimulation = async () => {
|
||||
if (!props.simulationId) return
|
||||
|
||||
isStopping.value = true
|
||||
addLog('正在停止模拟...')
|
||||
addLog(t('log.stoppingSim'))
|
||||
|
||||
try {
|
||||
const res = await stopSimulation({ simulation_id: props.simulationId })
|
||||
|
||||
if (res.success) {
|
||||
addLog('✓ 模拟已停止')
|
||||
addLog(t('log.simStoppedSuccess'))
|
||||
phase.value = 2
|
||||
stopPolling()
|
||||
emit('update-status', 'completed')
|
||||
} else {
|
||||
addLog(`停止失败: ${res.error || '未知错误'}`)
|
||||
addLog(t('log.stopFailed', { error: res.error || t('common.unknownError') }))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`停止异常: ${err.message}`)
|
||||
addLog(t('log.stopException', { error: err.message }))
|
||||
} finally {
|
||||
isStopping.value = false
|
||||
}
|
||||
@@ -517,9 +520,9 @@ const fetchRunStatus = async () => {
|
||||
|
||||
if (isCompleted || platformsCompleted) {
|
||||
if (platformsCompleted && !isCompleted) {
|
||||
addLog('✓ 检测到所有平台模拟已结束')
|
||||
addLog(t('log.allPlatformsCompleted'))
|
||||
}
|
||||
addLog('✓ 模拟已完成')
|
||||
addLog(t('log.simCompleted'))
|
||||
phase.value = 2
|
||||
stopPolling()
|
||||
emit('update-status', 'completed')
|
||||
@@ -640,17 +643,17 @@ const formatActionTime = (timestamp) => {
|
||||
|
||||
const handleNextStep = async () => {
|
||||
if (!props.simulationId) {
|
||||
addLog('错误:缺少 simulationId')
|
||||
addLog(t('log.errorMissingSimId'))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (isGeneratingReport.value) {
|
||||
addLog('报告生成请求已发送,请稍候...')
|
||||
addLog(t('log.reportRequestSent'))
|
||||
return
|
||||
}
|
||||
|
||||
isGeneratingReport.value = true
|
||||
addLog('正在启动报告生成...')
|
||||
addLog(t('log.startingReportGen'))
|
||||
|
||||
try {
|
||||
const res = await generateReport({
|
||||
@@ -660,16 +663,16 @@ const handleNextStep = async () => {
|
||||
|
||||
if (res.success && res.data) {
|
||||
const reportId = res.data.report_id
|
||||
addLog(`✓ 报告生成任务已启动: ${reportId}`)
|
||||
addLog(t('log.reportGenTaskStarted', { reportId }))
|
||||
|
||||
// 跳转到报告页面
|
||||
router.push({ name: 'Report', params: { reportId } })
|
||||
} else {
|
||||
addLog(`✗ 启动报告生成失败: ${res.error || '未知错误'}`)
|
||||
addLog(t('log.reportGenFailed', { error: res.error || t('common.unknownError') }))
|
||||
isGeneratingReport.value = false
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`✗ 启动报告生成异常: ${err.message}`)
|
||||
addLog(t('log.reportGenException', { error: err.message }))
|
||||
isGeneratingReport.value = false
|
||||
}
|
||||
}
|
||||
@@ -685,7 +688,7 @@ watch(() => props.systemLogs?.length, () => {
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
addLog('Step3 模拟运行初始化')
|
||||
addLog(t('log.step3Init'))
|
||||
if (props.simulationId) {
|
||||
doStartSimulation()
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<path d="M12 2a10 10 0 0 1 10 10" stroke-width="4" stroke="#4B5563" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="loading-text">正在生成{{ section.title }}...</span>
|
||||
<span class="loading-text">{{ $t('step4.generatingSection', { title: section.title }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -129,7 +129,7 @@
|
||||
|
||||
<!-- Next Step Button - 在完成后显示 -->
|
||||
<button v-if="isComplete" class="next-step-btn" @click="goToInteraction">
|
||||
<span>进入深度互动</span>
|
||||
<span>{{ $t('step4.goToInteraction') }}</span>
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="5" y1="12" x2="19" y2="12"></line>
|
||||
<polyline points="12 5 19 12 12 19"></polyline>
|
||||
@@ -392,9 +392,11 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick, h, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getAgentLog, getConsoleLog } from '../api/report'
|
||||
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
reportId: String,
|
||||
@@ -962,6 +964,7 @@ const parseQuickSearch = (text) => {
|
||||
const InsightDisplay = {
|
||||
props: ['result', 'resultLength'],
|
||||
setup(props) {
|
||||
const { t } = useI18n()
|
||||
const activeTab = ref('facts') // 'facts', 'entities', 'relations', 'subqueries'
|
||||
const expandedFacts = ref(false)
|
||||
const expandedEntities = ref(false)
|
||||
@@ -1003,7 +1006,7 @@ const InsightDisplay = {
|
||||
]),
|
||||
props.result.query && h('div', { class: 'header-topic' }, props.result.query),
|
||||
props.result.simulationRequirement && h('div', { class: 'header-scenario' }, [
|
||||
h('span', { class: 'scenario-label' }, '预测场景: '),
|
||||
h('span', { class: 'scenario-label' }, t('step4.scenarioLabel')),
|
||||
h('span', { class: 'scenario-text' }, props.result.simulationRequirement)
|
||||
])
|
||||
]),
|
||||
@@ -1014,25 +1017,25 @@ const InsightDisplay = {
|
||||
class: ['insight-tab', { active: activeTab.value === 'facts' }],
|
||||
onClick: () => { activeTab.value = 'facts' }
|
||||
}, [
|
||||
h('span', { class: 'tab-label' }, `当前关键记忆 (${props.result.facts.length})`)
|
||||
h('span', { class: 'tab-label' }, t('step4.tabKeyFacts', { count: props.result.facts.length }))
|
||||
]),
|
||||
h('button', {
|
||||
class: ['insight-tab', { active: activeTab.value === 'entities' }],
|
||||
onClick: () => { activeTab.value = 'entities' }
|
||||
}, [
|
||||
h('span', { class: 'tab-label' }, `核心实体 (${props.result.entities.length})`)
|
||||
h('span', { class: 'tab-label' }, t('step4.tabCoreEntities', { count: props.result.entities.length }))
|
||||
]),
|
||||
h('button', {
|
||||
class: ['insight-tab', { active: activeTab.value === 'relations' }],
|
||||
onClick: () => { activeTab.value = 'relations' }
|
||||
}, [
|
||||
h('span', { class: 'tab-label' }, `关系链 (${props.result.relations.length})`)
|
||||
h('span', { class: 'tab-label' }, t('step4.tabRelationChains', { count: props.result.relations.length }))
|
||||
]),
|
||||
props.result.subQueries.length > 0 && h('button', {
|
||||
class: ['insight-tab', { active: activeTab.value === 'subqueries' }],
|
||||
onClick: () => { activeTab.value = 'subqueries' }
|
||||
}, [
|
||||
h('span', { class: 'tab-label' }, `子问题 (${props.result.subQueries.length})`)
|
||||
h('span', { class: 'tab-label' }, t('step4.tabSubQueries', { count: props.result.subQueries.length }))
|
||||
])
|
||||
]),
|
||||
|
||||
@@ -1041,8 +1044,8 @@ const InsightDisplay = {
|
||||
// Facts Tab
|
||||
activeTab.value === 'facts' && props.result.facts.length > 0 && h('div', { class: 'facts-panel' }, [
|
||||
h('div', { class: 'panel-header' }, [
|
||||
h('span', { class: 'panel-title' }, '时序记忆中所关联的最新关键事实'),
|
||||
h('span', { class: 'panel-count' }, `共 ${props.result.facts.length} 条`)
|
||||
h('span', { class: 'panel-title' }, t('step4.panelKeyFacts')),
|
||||
h('span', { class: 'panel-count' }, t('step4.totalCount', { count: props.result.facts.length }))
|
||||
]),
|
||||
h('div', { class: 'facts-list' },
|
||||
(expandedFacts.value ? props.result.facts : props.result.facts.slice(0, INITIAL_SHOW_COUNT)).map((fact, i) =>
|
||||
@@ -1055,35 +1058,35 @@ const InsightDisplay = {
|
||||
props.result.facts.length > INITIAL_SHOW_COUNT && h('button', {
|
||||
class: 'expand-btn',
|
||||
onClick: () => { expandedFacts.value = !expandedFacts.value }
|
||||
}, expandedFacts.value ? `收起 ▲` : `展开全部 ${props.result.facts.length} 条 ▼`)
|
||||
}, expandedFacts.value ? t('step4.collapse') : t('step4.expandAll', { count: props.result.facts.length }))
|
||||
]),
|
||||
|
||||
|
||||
// Entities Tab
|
||||
activeTab.value === 'entities' && props.result.entities.length > 0 && h('div', { class: 'entities-panel' }, [
|
||||
h('div', { class: 'panel-header' }, [
|
||||
h('span', { class: 'panel-title' }, '核心实体'),
|
||||
h('span', { class: 'panel-count' }, `共 ${props.result.entities.length} 个`)
|
||||
h('span', { class: 'panel-title' }, t('step4.panelCoreEntities')),
|
||||
h('span', { class: 'panel-count' }, t('step4.totalEntityCount', { count: props.result.entities.length }))
|
||||
]),
|
||||
h('div', { class: 'entities-grid' },
|
||||
(expandedEntities.value ? props.result.entities : props.result.entities.slice(0, 12)).map((entity, i) =>
|
||||
h('div', { class: 'entity-tag', key: i, title: entity.summary || '' }, [
|
||||
h('span', { class: 'entity-name' }, entity.name),
|
||||
h('span', { class: 'entity-type' }, entity.type),
|
||||
entity.relatedFactsCount > 0 && h('span', { class: 'entity-fact-count' }, `${entity.relatedFactsCount}条`)
|
||||
entity.relatedFactsCount > 0 && h('span', { class: 'entity-fact-count' }, t('step4.factCount', { count: entity.relatedFactsCount }))
|
||||
])
|
||||
)
|
||||
),
|
||||
props.result.entities.length > 12 && h('button', {
|
||||
class: 'expand-btn',
|
||||
onClick: () => { expandedEntities.value = !expandedEntities.value }
|
||||
}, expandedEntities.value ? `收起 ▲` : `展开全部 ${props.result.entities.length} 个 ▼`)
|
||||
}, expandedEntities.value ? t('step4.collapse') : t('step4.expandAllEntities', { count: props.result.entities.length }))
|
||||
]),
|
||||
|
||||
|
||||
// Relations Tab
|
||||
activeTab.value === 'relations' && props.result.relations.length > 0 && h('div', { class: 'relations-panel' }, [
|
||||
h('div', { class: 'panel-header' }, [
|
||||
h('span', { class: 'panel-title' }, '关系链'),
|
||||
h('span', { class: 'panel-count' }, `共 ${props.result.relations.length} 条`)
|
||||
h('span', { class: 'panel-title' }, t('step4.panelRelationChains')),
|
||||
h('span', { class: 'panel-count' }, t('step4.totalCount', { count: props.result.relations.length }))
|
||||
]),
|
||||
h('div', { class: 'relations-list' },
|
||||
(expandedRelations.value ? props.result.relations : props.result.relations.slice(0, INITIAL_SHOW_COUNT)).map((rel, i) =>
|
||||
@@ -1101,14 +1104,14 @@ const InsightDisplay = {
|
||||
props.result.relations.length > INITIAL_SHOW_COUNT && h('button', {
|
||||
class: 'expand-btn',
|
||||
onClick: () => { expandedRelations.value = !expandedRelations.value }
|
||||
}, expandedRelations.value ? `收起 ▲` : `展开全部 ${props.result.relations.length} 条 ▼`)
|
||||
}, expandedRelations.value ? t('step4.collapse') : t('step4.expandAll', { count: props.result.relations.length }))
|
||||
]),
|
||||
|
||||
|
||||
// Sub-queries Tab
|
||||
activeTab.value === 'subqueries' && props.result.subQueries.length > 0 && h('div', { class: 'subqueries-panel' }, [
|
||||
h('div', { class: 'panel-header' }, [
|
||||
h('span', { class: 'panel-title' }, '漂移查询生成分析子问题'),
|
||||
h('span', { class: 'panel-count' }, `共 ${props.result.subQueries.length} 个`)
|
||||
h('span', { class: 'panel-title' }, t('step4.panelSubQueries')),
|
||||
h('span', { class: 'panel-count' }, t('step4.totalEntityCount', { count: props.result.subQueries.length }))
|
||||
]),
|
||||
h('div', { class: 'subqueries-list' },
|
||||
props.result.subQueries.map((sq, i) =>
|
||||
@@ -1121,9 +1124,9 @@ const InsightDisplay = {
|
||||
]),
|
||||
|
||||
// Empty state
|
||||
activeTab.value === 'facts' && props.result.facts.length === 0 && h('div', { class: 'empty-state' }, '暂无当前关键记忆'),
|
||||
activeTab.value === 'entities' && props.result.entities.length === 0 && h('div', { class: 'empty-state' }, '暂无核心实体'),
|
||||
activeTab.value === 'relations' && props.result.relations.length === 0 && h('div', { class: 'empty-state' }, '暂无关系链')
|
||||
activeTab.value === 'facts' && props.result.facts.length === 0 && h('div', { class: 'empty-state' }, t('step4.emptyKeyFacts')),
|
||||
activeTab.value === 'entities' && props.result.entities.length === 0 && h('div', { class: 'empty-state' }, t('step4.emptyCoreEntities')),
|
||||
activeTab.value === 'relations' && props.result.relations.length === 0 && h('div', { class: 'empty-state' }, t('step4.emptyRelationChains'))
|
||||
])
|
||||
])
|
||||
}
|
||||
@@ -1133,6 +1136,7 @@ const InsightDisplay = {
|
||||
const PanoramaDisplay = {
|
||||
props: ['result', 'resultLength'],
|
||||
setup(props) {
|
||||
const { t } = useI18n()
|
||||
const activeTab = ref('active') // 'active', 'historical', 'entities'
|
||||
const expandedActive = ref(false)
|
||||
const expandedHistorical = ref(false)
|
||||
@@ -1176,19 +1180,19 @@ const PanoramaDisplay = {
|
||||
class: ['panorama-tab', { active: activeTab.value === 'active' }],
|
||||
onClick: () => { activeTab.value = 'active' }
|
||||
}, [
|
||||
h('span', { class: 'tab-label' }, `当前有效记忆 (${props.result.activeFacts.length})`)
|
||||
h('span', { class: 'tab-label' }, t('step4.tabActiveFacts', { count: props.result.activeFacts.length }))
|
||||
]),
|
||||
h('button', {
|
||||
class: ['panorama-tab', { active: activeTab.value === 'historical' }],
|
||||
onClick: () => { activeTab.value = 'historical' }
|
||||
}, [
|
||||
h('span', { class: 'tab-label' }, `历史记忆 (${props.result.historicalFacts.length})`)
|
||||
h('span', { class: 'tab-label' }, t('step4.tabHistoricalFacts', { count: props.result.historicalFacts.length }))
|
||||
]),
|
||||
h('button', {
|
||||
class: ['panorama-tab', { active: activeTab.value === 'entities' }],
|
||||
onClick: () => { activeTab.value = 'entities' }
|
||||
}, [
|
||||
h('span', { class: 'tab-label' }, `涉及实体 (${props.result.entities.length})`)
|
||||
h('span', { class: 'tab-label' }, t('step4.tabEntities', { count: props.result.entities.length }))
|
||||
])
|
||||
]),
|
||||
|
||||
@@ -1197,8 +1201,8 @@ const PanoramaDisplay = {
|
||||
// Active Facts Tab
|
||||
activeTab.value === 'active' && h('div', { class: 'facts-panel active-facts' }, [
|
||||
h('div', { class: 'panel-header' }, [
|
||||
h('span', { class: 'panel-title' }, '当前有效记忆'),
|
||||
h('span', { class: 'panel-count' }, `共 ${props.result.activeFacts.length} 条`)
|
||||
h('span', { class: 'panel-title' }, t('step4.panelActiveFacts')),
|
||||
h('span', { class: 'panel-count' }, t('step4.totalCount', { count: props.result.activeFacts.length }))
|
||||
]),
|
||||
props.result.activeFacts.length > 0 ? h('div', { class: 'facts-list' },
|
||||
(expandedActive.value ? props.result.activeFacts : props.result.activeFacts.slice(0, INITIAL_SHOW_COUNT)).map((fact, i) =>
|
||||
@@ -1207,18 +1211,18 @@ const PanoramaDisplay = {
|
||||
h('div', { class: 'fact-content' }, fact)
|
||||
])
|
||||
)
|
||||
) : h('div', { class: 'empty-state' }, '暂无当前有效记忆'),
|
||||
) : h('div', { class: 'empty-state' }, t('step4.emptyActiveFacts')),
|
||||
props.result.activeFacts.length > INITIAL_SHOW_COUNT && h('button', {
|
||||
class: 'expand-btn',
|
||||
onClick: () => { expandedActive.value = !expandedActive.value }
|
||||
}, expandedActive.value ? `收起 ▲` : `展开全部 ${props.result.activeFacts.length} 条 ▼`)
|
||||
}, expandedActive.value ? t('step4.collapse') : t('step4.expandAll', { count: props.result.activeFacts.length }))
|
||||
]),
|
||||
|
||||
// Historical Facts Tab
|
||||
activeTab.value === 'historical' && h('div', { class: 'facts-panel historical-facts' }, [
|
||||
h('div', { class: 'panel-header' }, [
|
||||
h('span', { class: 'panel-title' }, '历史记忆'),
|
||||
h('span', { class: 'panel-count' }, `共 ${props.result.historicalFacts.length} 条`)
|
||||
h('span', { class: 'panel-title' }, t('step4.panelHistoricalFacts')),
|
||||
h('span', { class: 'panel-count' }, t('step4.totalCount', { count: props.result.historicalFacts.length }))
|
||||
]),
|
||||
props.result.historicalFacts.length > 0 ? h('div', { class: 'facts-list' },
|
||||
(expandedHistorical.value ? props.result.historicalFacts : props.result.historicalFacts.slice(0, INITIAL_SHOW_COUNT)).map((fact, i) =>
|
||||
@@ -1239,18 +1243,18 @@ const PanoramaDisplay = {
|
||||
])
|
||||
])
|
||||
)
|
||||
) : h('div', { class: 'empty-state' }, '暂无历史记忆'),
|
||||
) : h('div', { class: 'empty-state' }, t('step4.emptyHistoricalFacts')),
|
||||
props.result.historicalFacts.length > INITIAL_SHOW_COUNT && h('button', {
|
||||
class: 'expand-btn',
|
||||
onClick: () => { expandedHistorical.value = !expandedHistorical.value }
|
||||
}, expandedHistorical.value ? `收起 ▲` : `展开全部 ${props.result.historicalFacts.length} 条 ▼`)
|
||||
}, expandedHistorical.value ? t('step4.collapse') : t('step4.expandAll', { count: props.result.historicalFacts.length }))
|
||||
]),
|
||||
|
||||
// Entities Tab
|
||||
activeTab.value === 'entities' && h('div', { class: 'entities-panel' }, [
|
||||
h('div', { class: 'panel-header' }, [
|
||||
h('span', { class: 'panel-title' }, '涉及实体'),
|
||||
h('span', { class: 'panel-count' }, `共 ${props.result.entities.length} 个`)
|
||||
h('span', { class: 'panel-title' }, t('step4.panelEntities')),
|
||||
h('span', { class: 'panel-count' }, t('step4.totalEntityCount', { count: props.result.entities.length }))
|
||||
]),
|
||||
props.result.entities.length > 0 ? h('div', { class: 'entities-grid' },
|
||||
(expandedEntities.value ? props.result.entities : props.result.entities.slice(0, 8)).map((entity, i) =>
|
||||
@@ -1259,11 +1263,11 @@ const PanoramaDisplay = {
|
||||
entity.type && h('span', { class: 'entity-type' }, entity.type)
|
||||
])
|
||||
)
|
||||
) : h('div', { class: 'empty-state' }, '暂无涉及实体'),
|
||||
) : h('div', { class: 'empty-state' }, t('step4.emptyEntities')),
|
||||
props.result.entities.length > 8 && h('button', {
|
||||
class: 'expand-btn',
|
||||
onClick: () => { expandedEntities.value = !expandedEntities.value }
|
||||
}, expandedEntities.value ? `收起 ▲` : `展开全部 ${props.result.entities.length} 个 ▼`)
|
||||
}, expandedEntities.value ? t('step4.collapse') : t('step4.expandAllEntities', { count: props.result.entities.length }))
|
||||
])
|
||||
])
|
||||
])
|
||||
@@ -1512,7 +1516,7 @@ const InterviewDisplay = {
|
||||
h('line', { x1: '2', y1: '12', x2: '22', y2: '12' }),
|
||||
h('path', { d: 'M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z' })
|
||||
]),
|
||||
h('span', {}, '世界1')
|
||||
h('span', {}, t('step4.world1'))
|
||||
]),
|
||||
h('button', {
|
||||
class: ['platform-btn', { active: currentPlatform === 'reddit' }],
|
||||
@@ -1521,7 +1525,7 @@ const InterviewDisplay = {
|
||||
h('svg', { class: 'platform-icon', viewBox: '0 0 24 24', width: 12, height: 12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, [
|
||||
h('path', { d: 'M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z' })
|
||||
]),
|
||||
h('span', {}, '世界2')
|
||||
h('span', {}, t('step4.world2'))
|
||||
])
|
||||
])
|
||||
]),
|
||||
@@ -1577,6 +1581,7 @@ const InterviewDisplay = {
|
||||
const QuickSearchDisplay = {
|
||||
props: ['result', 'resultLength'],
|
||||
setup(props) {
|
||||
const { t } = useI18n()
|
||||
const activeTab = ref('facts') // 'facts', 'edges', 'nodes'
|
||||
const expandedFacts = ref(false)
|
||||
const INITIAL_SHOW_COUNT = 5
|
||||
@@ -1610,7 +1615,7 @@ const QuickSearchDisplay = {
|
||||
])
|
||||
]),
|
||||
props.result.query && h('div', { class: 'header-query' }, [
|
||||
h('span', { class: 'query-label' }, '搜索: '),
|
||||
h('span', { class: 'query-label' }, t('step4.searchLabel')),
|
||||
h('span', { class: 'query-text' }, props.result.query)
|
||||
])
|
||||
]),
|
||||
@@ -1621,19 +1626,19 @@ const QuickSearchDisplay = {
|
||||
class: ['quicksearch-tab', { active: activeTab.value === 'facts' }],
|
||||
onClick: () => { activeTab.value = 'facts' }
|
||||
}, [
|
||||
h('span', { class: 'tab-label' }, `事实 (${props.result.facts.length})`)
|
||||
h('span', { class: 'tab-label' }, t('step4.tabFacts', { count: props.result.facts.length }))
|
||||
]),
|
||||
hasEdges.value && h('button', {
|
||||
class: ['quicksearch-tab', { active: activeTab.value === 'edges' }],
|
||||
onClick: () => { activeTab.value = 'edges' }
|
||||
}, [
|
||||
h('span', { class: 'tab-label' }, `关系 (${props.result.edges.length})`)
|
||||
h('span', { class: 'tab-label' }, t('step4.tabEdges', { count: props.result.edges.length }))
|
||||
]),
|
||||
hasNodes.value && h('button', {
|
||||
class: ['quicksearch-tab', { active: activeTab.value === 'nodes' }],
|
||||
onClick: () => { activeTab.value = 'nodes' }
|
||||
}, [
|
||||
h('span', { class: 'tab-label' }, `节点 (${props.result.nodes.length})`)
|
||||
h('span', { class: 'tab-label' }, t('step4.tabNodes', { count: props.result.nodes.length }))
|
||||
])
|
||||
]),
|
||||
|
||||
@@ -1642,8 +1647,8 @@ const QuickSearchDisplay = {
|
||||
// Facts (always show if no tabs, or when facts tab is active)
|
||||
((!showTabs.value) || activeTab.value === 'facts') && h('div', { class: 'facts-panel' }, [
|
||||
!showTabs.value && h('div', { class: 'panel-header' }, [
|
||||
h('span', { class: 'panel-title' }, '搜索结果'),
|
||||
h('span', { class: 'panel-count' }, `共 ${props.result.facts.length} 条`)
|
||||
h('span', { class: 'panel-title' }, t('step4.panelSearchResults')),
|
||||
h('span', { class: 'panel-count' }, t('step4.totalCount', { count: props.result.facts.length }))
|
||||
]),
|
||||
props.result.facts.length > 0 ? h('div', { class: 'facts-list' },
|
||||
(expandedFacts.value ? props.result.facts : props.result.facts.slice(0, INITIAL_SHOW_COUNT)).map((fact, i) =>
|
||||
@@ -1652,18 +1657,18 @@ const QuickSearchDisplay = {
|
||||
h('div', { class: 'fact-content' }, fact)
|
||||
])
|
||||
)
|
||||
) : h('div', { class: 'empty-state' }, '未找到相关结果'),
|
||||
) : h('div', { class: 'empty-state' }, t('step4.emptySearchResults')),
|
||||
props.result.facts.length > INITIAL_SHOW_COUNT && h('button', {
|
||||
class: 'expand-btn',
|
||||
onClick: () => { expandedFacts.value = !expandedFacts.value }
|
||||
}, expandedFacts.value ? `收起 ▲` : `展开全部 ${props.result.facts.length} 条 ▼`)
|
||||
}, expandedFacts.value ? t('step4.collapse') : t('step4.expandAll', { count: props.result.facts.length }))
|
||||
]),
|
||||
|
||||
// Edges Tab
|
||||
activeTab.value === 'edges' && hasEdges.value && h('div', { class: 'edges-panel' }, [
|
||||
h('div', { class: 'panel-header' }, [
|
||||
h('span', { class: 'panel-title' }, '相关关系'),
|
||||
h('span', { class: 'panel-count' }, `共 ${props.result.edges.length} 条`)
|
||||
h('span', { class: 'panel-title' }, t('step4.panelRelatedEdges')),
|
||||
h('span', { class: 'panel-count' }, t('step4.totalCount', { count: props.result.edges.length }))
|
||||
]),
|
||||
h('div', { class: 'edges-list' },
|
||||
props.result.edges.map((edge, i) =>
|
||||
@@ -1683,8 +1688,8 @@ const QuickSearchDisplay = {
|
||||
// Nodes Tab
|
||||
activeTab.value === 'nodes' && hasNodes.value && h('div', { class: 'nodes-panel' }, [
|
||||
h('div', { class: 'panel-header' }, [
|
||||
h('span', { class: 'panel-title' }, '相关节点'),
|
||||
h('span', { class: 'panel-count' }, `共 ${props.result.nodes.length} 个`)
|
||||
h('span', { class: 'panel-title' }, t('step4.panelRelatedNodes')),
|
||||
h('span', { class: 'panel-count' }, t('step4.totalEntityCount', { count: props.result.nodes.length }))
|
||||
]),
|
||||
h('div', { class: 'nodes-grid' },
|
||||
props.result.nodes.map((node, i) =>
|
||||
@@ -5148,3 +5153,10 @@ watch(() => props.reportId, (newId) => {
|
||||
.log-msg.warning { color: #FFA726; }
|
||||
.log-msg.success { color: #66BB6A; }
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* English locale: smaller report title */
|
||||
html[lang="en"] .report-header-block .main-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<path d="M12 2a10 10 0 0 1 10 10" stroke-width="4" stroke="#4B5563" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="loading-text">正在生成{{ section.title }}...</span>
|
||||
<span class="loading-text">{{ $t('step4.generatingSection', { title: section.title }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -85,8 +85,8 @@
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
<div class="action-bar-text">
|
||||
<span class="action-bar-title">Interactive Tools</span>
|
||||
<span class="action-bar-subtitle mono">{{ profiles.length }} agents available</span>
|
||||
<span class="action-bar-title">{{ $t('step5.interactiveTools') }}</span>
|
||||
<span class="action-bar-subtitle mono">{{ $t('step5.agentsAvailable', { count: profiles.length }) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-bar-tabs">
|
||||
@@ -98,7 +98,7 @@
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path>
|
||||
</svg>
|
||||
<span>与Report Agent对话</span>
|
||||
<span>{{ $t('step5.chatWithReportAgent') }}</span>
|
||||
</button>
|
||||
<div class="agent-dropdown" v-if="profiles.length > 0">
|
||||
<button
|
||||
@@ -110,13 +110,13 @@
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
<span>{{ selectedAgent ? selectedAgent.username : '与世界中任意个体对话' }}</span>
|
||||
<span>{{ selectedAgent ? selectedAgent.username : $t('step5.chatWithAgent') }}</span>
|
||||
<svg class="dropdown-arrow" :class="{ open: showAgentDropdown }" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showAgentDropdown" class="dropdown-menu">
|
||||
<div class="dropdown-header">选择对话对象</div>
|
||||
<div class="dropdown-header">{{ $t('step5.selectChatTarget') }}</div>
|
||||
<div
|
||||
v-for="(agent, idx) in profiles"
|
||||
:key="idx"
|
||||
@@ -126,13 +126,13 @@
|
||||
<div class="agent-avatar">{{ (agent.username || 'A')[0] }}</div>
|
||||
<div class="agent-info">
|
||||
<span class="agent-name">{{ agent.username }}</span>
|
||||
<span class="agent-role">{{ agent.profession || '未知职业' }}</span>
|
||||
<span class="agent-role">{{ agent.profession || $t('step2.unknownProfession') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-divider"></div>
|
||||
<button
|
||||
<button
|
||||
class="tab-pill survey-pill"
|
||||
:class="{ active: activeTab === 'survey' }"
|
||||
@click="selectSurveyTab"
|
||||
@@ -141,7 +141,7 @@
|
||||
<path d="M9 11l3 3L22 4"></path>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
||||
</svg>
|
||||
<span>发送问卷调查到世界中</span>
|
||||
<span>{{ $t('step5.sendSurvey') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,8 +154,8 @@
|
||||
<div class="tools-card-header">
|
||||
<div class="tools-card-avatar">R</div>
|
||||
<div class="tools-card-info">
|
||||
<div class="tools-card-name">Report Agent - Chat</div>
|
||||
<div class="tools-card-subtitle">报告生成智能体的快速对话版本,可调用 4 种专业工具,拥有MiroFish的完整记忆</div>
|
||||
<div class="tools-card-name">{{ $t('step5.reportAgentChat') }}</div>
|
||||
<div class="tools-card-subtitle">{{ $t('step5.reportAgentDesc') }}</div>
|
||||
</div>
|
||||
<button class="tools-card-toggle" @click="showToolsDetail = !showToolsDetail">
|
||||
<svg :class="{ 'is-expanded': showToolsDetail }" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
||||
@@ -172,8 +172,8 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="tool-content">
|
||||
<div class="tool-name">InsightForge 深度归因</div>
|
||||
<div class="tool-desc">对齐现实世界种子数据与模拟环境状态,结合Global/Local Memory机制,提供跨时空的深度归因分析</div>
|
||||
<div class="tool-name">{{ $t('step5.toolInsightForge') }}</div>
|
||||
<div class="tool-desc">{{ $t('step5.toolInsightForgeDesc') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-item tool-blue">
|
||||
@@ -184,8 +184,8 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="tool-content">
|
||||
<div class="tool-name">PanoramaSearch 全景追踪</div>
|
||||
<div class="tool-desc">基于图结构的广度遍历算法,重构事件传播路径,捕获全量信息流动的拓扑结构</div>
|
||||
<div class="tool-name">{{ $t('step5.toolPanoramaSearch') }}</div>
|
||||
<div class="tool-desc">{{ $t('step5.toolPanoramaSearchDesc') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-item tool-orange">
|
||||
@@ -195,8 +195,8 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="tool-content">
|
||||
<div class="tool-name">QuickSearch 快速检索</div>
|
||||
<div class="tool-desc">基于 GraphRAG 的即时查询接口,优化索引效率,用于快速提取具体的节点属性与离散事实</div>
|
||||
<div class="tool-name">{{ $t('step5.toolQuickSearch') }}</div>
|
||||
<div class="tool-desc">{{ $t('step5.toolQuickSearchDesc') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tool-item tool-green">
|
||||
@@ -208,8 +208,8 @@
|
||||
</svg>
|
||||
</div>
|
||||
<div class="tool-content">
|
||||
<div class="tool-name">InterviewSubAgent 虚拟访谈</div>
|
||||
<div class="tool-desc">自主式访谈,能够并行与模拟世界中个体进行多轮对话,采集非结构化的观点数据与心理状态</div>
|
||||
<div class="tool-name">{{ $t('step5.toolInterviewSubAgent') }}</div>
|
||||
<div class="tool-desc">{{ $t('step5.toolInterviewSubAgentDesc') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,7 +224,7 @@
|
||||
<div class="profile-card-name">{{ selectedAgent.username }}</div>
|
||||
<div class="profile-card-meta">
|
||||
<span v-if="selectedAgent.name" class="profile-card-handle">@{{ selectedAgent.name }}</span>
|
||||
<span class="profile-card-profession">{{ selectedAgent.profession || '未知职业' }}</span>
|
||||
<span class="profile-card-profession">{{ selectedAgent.profession || $t('step2.unknownProfession') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="profile-card-toggle" @click="showFullProfile = !showFullProfile">
|
||||
@@ -235,7 +235,7 @@
|
||||
</div>
|
||||
<div v-if="showFullProfile && selectedAgent.bio" class="profile-card-body">
|
||||
<div class="profile-card-bio">
|
||||
<div class="profile-card-label">简介</div>
|
||||
<div class="profile-card-label">{{ $t('step5.profileBio') }}</div>
|
||||
<p>{{ selectedAgent.bio }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -250,7 +250,7 @@
|
||||
</svg>
|
||||
</div>
|
||||
<p class="empty-text">
|
||||
{{ chatTarget === 'report_agent' ? '与 Report Agent 对话,深入了解报告内容' : '与模拟个体对话,了解他们的观点' }}
|
||||
{{ chatTarget === 'report_agent' ? $t('step5.chatEmptyReportAgent') : $t('step5.chatEmptyAgent') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
@@ -292,7 +292,7 @@
|
||||
<textarea
|
||||
v-model="chatInput"
|
||||
class="chat-input"
|
||||
placeholder="输入您的问题..."
|
||||
:placeholder="$t('step5.chatInputPlaceholder')"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
:disabled="isSending || (!selectedAgent && chatTarget === 'agent')"
|
||||
rows="1"
|
||||
@@ -317,8 +317,8 @@
|
||||
<div class="survey-setup">
|
||||
<div class="setup-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">选择调查对象</span>
|
||||
<span class="selection-count">已选 {{ selectedAgents.size }} / {{ profiles.length }}</span>
|
||||
<span class="section-title">{{ $t('step5.selectSurveyTarget') }}</span>
|
||||
<span class="selection-count">{{ $t('step5.selectedCount', { selected: selectedAgents.size, total: profiles.length }) }}</span>
|
||||
</div>
|
||||
<div class="agents-grid">
|
||||
<label
|
||||
@@ -335,7 +335,7 @@
|
||||
<div class="checkbox-avatar">{{ (agent.username || 'A')[0] }}</div>
|
||||
<div class="checkbox-info">
|
||||
<span class="checkbox-name">{{ agent.username }}</span>
|
||||
<span class="checkbox-role">{{ agent.profession || '未知职业' }}</span>
|
||||
<span class="checkbox-role">{{ agent.profession || $t('step2.unknownProfession') }}</span>
|
||||
</div>
|
||||
<div class="checkbox-indicator">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="3">
|
||||
@@ -345,20 +345,20 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="selection-actions">
|
||||
<button class="action-link" @click="selectAllAgents">全选</button>
|
||||
<button class="action-link" @click="selectAllAgents">{{ $t('step5.selectAll') }}</button>
|
||||
<span class="action-divider">|</span>
|
||||
<button class="action-link" @click="clearAgentSelection">清空</button>
|
||||
<button class="action-link" @click="clearAgentSelection">{{ $t('step5.clearSelection') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setup-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">问卷问题</span>
|
||||
<span class="section-title">{{ $t('step5.surveyQuestions') }}</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="surveyQuestion"
|
||||
class="survey-input"
|
||||
placeholder="输入您想问所有被选中对象的问题..."
|
||||
:placeholder="$t('step5.surveyInputPlaceholder')"
|
||||
rows="3"
|
||||
></textarea>
|
||||
</div>
|
||||
@@ -369,15 +369,15 @@
|
||||
@click="submitSurvey"
|
||||
>
|
||||
<span v-if="isSurveying" class="loading-spinner"></span>
|
||||
<span v-else>发送问卷</span>
|
||||
<span v-else>{{ $t('step5.submitSurvey') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Survey Results -->
|
||||
<div v-if="surveyResults.length > 0" class="survey-results">
|
||||
<div class="results-header">
|
||||
<span class="results-title">调查结果</span>
|
||||
<span class="results-count">{{ surveyResults.length }} 条回复</span>
|
||||
<span class="results-title">{{ $t('step5.surveyResults') }}</span>
|
||||
<span class="results-count">{{ $t('step5.surveyResultsCount', { count: surveyResults.length }) }}</span>
|
||||
</div>
|
||||
<div class="results-list">
|
||||
<div
|
||||
@@ -389,7 +389,7 @@
|
||||
<div class="result-avatar">{{ (result.agent_name || 'A')[0] }}</div>
|
||||
<div class="result-info">
|
||||
<span class="result-name">{{ result.agent_name }}</span>
|
||||
<span class="result-role">{{ result.profession || '未知职业' }}</span>
|
||||
<span class="result-role">{{ result.profession || $t('step2.unknownProfession') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-question">
|
||||
@@ -412,9 +412,12 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { chatWithReport, getReport, getAgentLog } from '../api/report'
|
||||
import { interviewAgents, getSimulationProfilesRealtime } from '../api/simulation'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const props = defineProps({
|
||||
reportId: String,
|
||||
simulationId: String
|
||||
@@ -535,7 +538,7 @@ const selectAgent = (agent, idx) => {
|
||||
|
||||
// 恢复该 Agent 的对话记录
|
||||
chatHistory.value = chatHistoryCache.value[`agent_${idx}`] || []
|
||||
addLog(`选择对话对象: ${agent.username}`)
|
||||
addLog(t('log.selectChatTarget', { name: agent.username }))
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
@@ -662,10 +665,10 @@ const sendMessage = async () => {
|
||||
await sendToAgent(message)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`发送失败: ${err.message}`)
|
||||
addLog(t('log.sendFailed', { error: err.message }))
|
||||
chatHistory.value.push({
|
||||
role: 'assistant',
|
||||
content: `抱歉,发生了错误: ${err.message}`,
|
||||
content: t('step5.errorOccurred', { error: err.message }),
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} finally {
|
||||
@@ -677,7 +680,7 @@ const sendMessage = async () => {
|
||||
}
|
||||
|
||||
const sendToReportAgent = async (message) => {
|
||||
addLog(`向 Report Agent 发送: ${message.substring(0, 50)}...`)
|
||||
addLog(t('log.sendToReportAgent', { message: message.substring(0, 50) }))
|
||||
|
||||
// Build chat history for API
|
||||
const historyForApi = chatHistory.value
|
||||
@@ -697,21 +700,21 @@ const sendToReportAgent = async (message) => {
|
||||
if (res.success && res.data) {
|
||||
chatHistory.value.push({
|
||||
role: 'assistant',
|
||||
content: res.data.response || res.data.answer || '无响应',
|
||||
content: res.data.response || res.data.answer || t('step5.noResponse'),
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
addLog('Report Agent 已回复')
|
||||
addLog(t('log.reportAgentReplied'))
|
||||
} else {
|
||||
throw new Error(res.error || '请求失败')
|
||||
throw new Error(res.error || t('step5.requestFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const sendToAgent = async (message) => {
|
||||
if (!selectedAgent.value || selectedAgentIndex.value === null) {
|
||||
throw new Error('请先选择一个模拟个体')
|
||||
throw new Error(t('step5.selectAgentFirst'))
|
||||
}
|
||||
|
||||
addLog(`向 ${selectedAgent.value.username} 发送: ${message.substring(0, 50)}...`)
|
||||
addLog(t('log.sendToAgent', { name: selectedAgent.value.username, message: message.substring(0, 50) }))
|
||||
|
||||
// Build prompt with chat history
|
||||
let prompt = message
|
||||
@@ -761,12 +764,12 @@ const sendToAgent = async (message) => {
|
||||
content: responseContent,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
addLog(`${selectedAgent.value.username} 已回复`)
|
||||
addLog(t('log.agentReplied', { name: selectedAgent.value.username }))
|
||||
} else {
|
||||
throw new Error('无响应数据')
|
||||
throw new Error(t('step5.noResponse'))
|
||||
}
|
||||
} else {
|
||||
throw new Error(res.error || '请求失败')
|
||||
throw new Error(res.error || t('step5.requestFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,7 +806,7 @@ const submitSurvey = async () => {
|
||||
if (selectedAgents.value.size === 0 || !surveyQuestion.value.trim()) return
|
||||
|
||||
isSurveying.value = true
|
||||
addLog(`发送问卷给 ${selectedAgents.value.size} 个对象...`)
|
||||
addLog(t('log.sendSurvey', { count: selectedAgents.value.size }))
|
||||
|
||||
try {
|
||||
const interviews = Array.from(selectedAgents.value).map(idx => ({
|
||||
@@ -830,20 +833,20 @@ const submitSurvey = async () => {
|
||||
const agent = profiles.value[agentIdx]
|
||||
|
||||
// 优先使用 reddit 平台回复,其次 twitter
|
||||
let responseContent = '无响应'
|
||||
|
||||
let responseContent = t('step5.noResponse')
|
||||
|
||||
if (typeof resultsDict === 'object' && !Array.isArray(resultsDict)) {
|
||||
const redditKey = `reddit_${agentIdx}`
|
||||
const twitterKey = `twitter_${agentIdx}`
|
||||
const agentResult = resultsDict[redditKey] || resultsDict[twitterKey]
|
||||
if (agentResult) {
|
||||
responseContent = agentResult.response || agentResult.answer || '无响应'
|
||||
responseContent = agentResult.response || agentResult.answer || t('step5.noResponse')
|
||||
}
|
||||
} else if (Array.isArray(resultsDict)) {
|
||||
// 兼容数组格式
|
||||
const matchedResult = resultsDict.find(r => r.agent_id === agentIdx)
|
||||
if (matchedResult) {
|
||||
responseContent = matchedResult.response || matchedResult.answer || '无响应'
|
||||
responseContent = matchedResult.response || matchedResult.answer || t('step5.noResponse')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -857,12 +860,12 @@ const submitSurvey = async () => {
|
||||
}
|
||||
|
||||
surveyResults.value = surveyResultsList
|
||||
addLog(`收到 ${surveyResults.value.length} 条回复`)
|
||||
addLog(t('log.receivedReplies', { count: surveyResults.value.length }))
|
||||
} else {
|
||||
throw new Error(res.error || '请求失败')
|
||||
throw new Error(res.error || t('step5.requestFailed'))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`问卷发送失败: ${err.message}`)
|
||||
addLog(t('log.surveySendFailed', { error: err.message }))
|
||||
} finally {
|
||||
isSurveying.value = false
|
||||
}
|
||||
@@ -873,7 +876,7 @@ const loadReportData = async () => {
|
||||
if (!props.reportId) return
|
||||
|
||||
try {
|
||||
addLog(`加载报告数据: ${props.reportId}`)
|
||||
addLog(t('log.loadReportData', { id: props.reportId }))
|
||||
|
||||
// Get report info
|
||||
const reportRes = await getReport(props.reportId)
|
||||
@@ -882,7 +885,7 @@ const loadReportData = async () => {
|
||||
await loadAgentLogs()
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载报告失败: ${err.message}`)
|
||||
addLog(t('log.loadReportFailed', { error: err.message }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -904,10 +907,10 @@ const loadAgentLogs = async () => {
|
||||
}
|
||||
})
|
||||
|
||||
addLog('报告数据加载完成')
|
||||
addLog(t('log.reportDataLoaded'))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载报告日志失败: ${err.message}`)
|
||||
addLog(t('log.loadReportLogFailed', { error: err.message }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,10 +921,10 @@ const loadProfiles = async () => {
|
||||
const res = await getSimulationProfilesRealtime(props.simulationId, 'reddit')
|
||||
if (res.success && res.data) {
|
||||
profiles.value = res.data.profiles || []
|
||||
addLog(`加载了 ${profiles.value.length} 个模拟个体`)
|
||||
addLog(t('log.loadedProfiles', { count: profiles.value.length }))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载模拟个体失败: ${err.message}`)
|
||||
addLog(t('log.loadProfilesFailed', { error: err.message }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -935,7 +938,7 @@ const handleClickOutside = (e) => {
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
addLog('Step5 深度互动初始化')
|
||||
addLog(t('log.step5Init'))
|
||||
loadReportData()
|
||||
loadProfiles()
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
@@ -2572,3 +2575,10 @@ watch(() => props.simulationId, (newId) => {
|
||||
margin: 24px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* English locale: smaller report title */
|
||||
html[lang="en"] .report-header-block .main-title {
|
||||
font-size: 28px;
|
||||
}
|
||||
</style>
|
||||
|
||||
27
frontend/src/i18n/index.js
Normal file
27
frontend/src/i18n/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import languages from '../../../locales/languages.json'
|
||||
|
||||
const localeFiles = import.meta.glob('../../../locales/!(languages).json', { eager: true })
|
||||
|
||||
const messages = {}
|
||||
const availableLocales = []
|
||||
|
||||
for (const path in localeFiles) {
|
||||
const key = path.match(/\/([^/]+)\.json$/)[1]
|
||||
if (languages[key]) {
|
||||
messages[key] = localeFiles[path].default
|
||||
availableLocales.push({ key, label: languages[key].label })
|
||||
}
|
||||
}
|
||||
|
||||
const savedLocale = localStorage.getItem('locale') || 'zh'
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: savedLocale,
|
||||
fallbackLocale: 'zh',
|
||||
messages
|
||||
})
|
||||
|
||||
export { availableLocales }
|
||||
export default i18n
|
||||
@@ -1,9 +1,11 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import i18n from './i18n'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(i18n)
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
<nav class="navbar">
|
||||
<div class="nav-brand">MIROFISH</div>
|
||||
<div class="nav-links">
|
||||
<LanguageSwitcher />
|
||||
<a href="https://github.com/666ghj/MiroFish" target="_blank" class="github-link">
|
||||
访问我们的Github主页 <span class="arrow">↗</span>
|
||||
{{ $t('nav.visitGithub') }} <span class="arrow">↗</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -15,21 +16,25 @@
|
||||
<section class="hero-section">
|
||||
<div class="hero-left">
|
||||
<div class="tag-row">
|
||||
<span class="orange-tag">简洁通用的群体智能引擎</span>
|
||||
<span class="version-text">/ v0.1-预览版</span>
|
||||
<span class="orange-tag">{{ $t('home.tagline') }}</span>
|
||||
<span class="version-text">{{ $t('home.version') }}</span>
|
||||
</div>
|
||||
|
||||
<h1 class="main-title">
|
||||
上传任意报告<br>
|
||||
<span class="gradient-text">即刻推演未来</span>
|
||||
{{ $t('home.heroTitle1') }}<br>
|
||||
<span class="gradient-text">{{ $t('home.heroTitle2') }}</span>
|
||||
</h1>
|
||||
|
||||
<div class="hero-desc">
|
||||
<p>
|
||||
即使只有一段文字,<span class="highlight-bold">MiroFish</span> 也能基于其中的现实种子,全自动生成与之对应的至多<span class="highlight-orange">百万级Agent</span>构成的平行世界。通过上帝视角注入变量,在复杂的群体交互中寻找动态环境下的<span class="highlight-code">“局部最优解”</span>
|
||||
<i18n-t keypath="home.heroDesc" tag="span">
|
||||
<template #brand><span class="highlight-bold">{{ $t('home.heroDescBrand') }}</span></template>
|
||||
<template #agentScale><span class="highlight-orange">{{ $t('home.heroDescAgentScale') }}</span></template>
|
||||
<template #optimalSolution><span class="highlight-code">{{ $t('home.heroDescOptimalSolution') }}</span></template>
|
||||
</i18n-t>
|
||||
</p>
|
||||
<p class="slogan-text">
|
||||
让未来在 Agent 群中预演,让决策在百战后胜出<span class="blinking-cursor">_</span>
|
||||
{{ $t('home.slogan') }}<span class="blinking-cursor">_</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -53,65 +58,65 @@
|
||||
<!-- 左栏:状态与步骤 -->
|
||||
<div class="left-panel">
|
||||
<div class="panel-header">
|
||||
<span class="status-dot">■</span> 系统状态
|
||||
<span class="status-dot">■</span> {{ $t('home.systemStatus') }}
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">准备就绪</h2>
|
||||
<h2 class="section-title">{{ $t('home.systemReady') }}</h2>
|
||||
<p class="section-desc">
|
||||
预测引擎待命中,可上传多份非结构化数据以初始化模拟序列
|
||||
{{ $t('home.systemReadyDesc') }}
|
||||
</p>
|
||||
|
||||
<!-- 数据指标卡片 -->
|
||||
<div class="metrics-row">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">低成本</div>
|
||||
<div class="metric-label">常规模拟平均5$/次</div>
|
||||
<div class="metric-value">{{ $t('home.metricLowCost') }}</div>
|
||||
<div class="metric-label">{{ $t('home.metricLowCostDesc') }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">高可用</div>
|
||||
<div class="metric-label">最多百万级Agent模拟</div>
|
||||
<div class="metric-value">{{ $t('home.metricHighAvail') }}</div>
|
||||
<div class="metric-label">{{ $t('home.metricHighAvailDesc') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目模拟步骤介绍 (新增区域) -->
|
||||
<div class="steps-container">
|
||||
<div class="steps-header">
|
||||
<span class="diamond-icon">◇</span> 工作流序列
|
||||
<span class="diamond-icon">◇</span> {{ $t('home.workflowSequence') }}
|
||||
</div>
|
||||
<div class="workflow-list">
|
||||
<div class="workflow-item">
|
||||
<span class="step-num">01</span>
|
||||
<div class="step-info">
|
||||
<div class="step-title">图谱构建</div>
|
||||
<div class="step-desc">现实种子提取 & 个体与群体记忆注入 & GraphRAG构建</div>
|
||||
<div class="step-title">{{ $t('home.step01Title') }}</div>
|
||||
<div class="step-desc">{{ $t('home.step01Desc') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-item">
|
||||
<span class="step-num">02</span>
|
||||
<div class="step-info">
|
||||
<div class="step-title">环境搭建</div>
|
||||
<div class="step-desc">实体关系抽取 & 人设生成 & 环境配置Agent注入仿真参数</div>
|
||||
<div class="step-title">{{ $t('home.step02Title') }}</div>
|
||||
<div class="step-desc">{{ $t('home.step02Desc') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-item">
|
||||
<span class="step-num">03</span>
|
||||
<div class="step-info">
|
||||
<div class="step-title">开始模拟</div>
|
||||
<div class="step-desc">双平台并行模拟 & 自动解析预测需求 & 动态更新时序记忆</div>
|
||||
<div class="step-title">{{ $t('home.step03Title') }}</div>
|
||||
<div class="step-desc">{{ $t('home.step03Desc') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-item">
|
||||
<span class="step-num">04</span>
|
||||
<div class="step-info">
|
||||
<div class="step-title">报告生成</div>
|
||||
<div class="step-desc">ReportAgent拥有丰富的工具集与模拟后环境进行深度交互</div>
|
||||
<div class="step-title">{{ $t('home.step04Title') }}</div>
|
||||
<div class="step-desc">{{ $t('home.step04Desc') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-item">
|
||||
<span class="step-num">05</span>
|
||||
<div class="step-info">
|
||||
<div class="step-title">深度互动</div>
|
||||
<div class="step-desc">与模拟世界中的任意一位进行对话 & 与ReportAgent进行对话</div>
|
||||
<div class="step-title">{{ $t('home.step05Title') }}</div>
|
||||
<div class="step-desc">{{ $t('home.step05Desc') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,8 +129,8 @@
|
||||
<!-- 上传区域 -->
|
||||
<div class="console-section">
|
||||
<div class="console-header">
|
||||
<span class="console-label">01 / 现实种子</span>
|
||||
<span class="console-meta">支持格式: PDF, MD, TXT</span>
|
||||
<span class="console-label">{{ $t('home.realitySeed') }}</span>
|
||||
<span class="console-meta">{{ $t('home.supportedFormats') }}</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -148,8 +153,8 @@
|
||||
|
||||
<div v-if="files.length === 0" class="upload-placeholder">
|
||||
<div class="upload-icon">↑</div>
|
||||
<div class="upload-title">拖拽文件上传</div>
|
||||
<div class="upload-hint">或点击浏览文件系统</div>
|
||||
<div class="upload-title">{{ $t('home.dragToUpload') }}</div>
|
||||
<div class="upload-hint">{{ $t('home.orBrowse') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="file-list">
|
||||
@@ -164,23 +169,23 @@
|
||||
|
||||
<!-- 分割线 -->
|
||||
<div class="console-divider">
|
||||
<span>输入参数</span>
|
||||
<span>{{ $t('home.inputParams') }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="console-section">
|
||||
<div class="console-header">
|
||||
<span class="console-label">>_ 02 / 模拟提示词</span>
|
||||
<span class="console-label">{{ $t('home.simulationPrompt') }}</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
v-model="formData.simulationRequirement"
|
||||
class="code-input"
|
||||
placeholder="// 用自然语言输入模拟或预测需求(例.武大若发布撤销肖某处分的公告,会引发什么舆情走向)"
|
||||
:placeholder="$t('home.promptPlaceholder')"
|
||||
rows="6"
|
||||
:disabled="loading"
|
||||
></textarea>
|
||||
<div class="model-badge">引擎: MiroFish-V1.0</div>
|
||||
<div class="model-badge">{{ $t('home.engineBadge') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -191,8 +196,8 @@
|
||||
@click="startSimulation"
|
||||
:disabled="!canSubmit || loading"
|
||||
>
|
||||
<span v-if="!loading">启动引擎</span>
|
||||
<span v-else>初始化中...</span>
|
||||
<span v-if="!loading">{{ $t('home.startEngine') }}</span>
|
||||
<span v-else>{{ $t('home.initializing') }}</span>
|
||||
<span class="btn-arrow">→</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -210,6 +215,7 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import HistoryDatabase from '../components/HistoryDatabase.vue'
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -351,6 +357,7 @@ const startSimulation = () => {
|
||||
.nav-links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.github-link {
|
||||
@@ -888,3 +895,59 @@ const startSimulation = () => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
/* English locale adjustments (unscoped to target html[lang]) */
|
||||
html[lang="en"] .main-title {
|
||||
font-size: 3.5rem;
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
html[lang="en"] .hero-desc {
|
||||
text-align: left;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
html[lang="en"] .slogan-text {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
html[lang="en"] .tag-row {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
html[lang="en"] .navbar .nav-links {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
/* Left pane: system status + workflow */
|
||||
html[lang="en"] .status-section {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
html[lang="en"] .status-section .status-ready {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
html[lang="en"] .status-section .metric-value {
|
||||
font-family: 'Space Grotesk', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
html[lang="en"] .workflow-list .step-title {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
|
||||
html[lang="en"] .workflow-list .step-desc {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
||||
font-size: 0.72rem !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
html[lang="en"] .workflow-list {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,15 +15,17 @@
|
||||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: $t('main.layoutGraph'), split: $t('main.layoutSplit'), workbench: $t('main.layoutWorkbench') }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<LanguageSwitcher />
|
||||
<div class="step-divider"></div>
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 5/5</span>
|
||||
<span class="step-name">深度互动</span>
|
||||
<span class="step-name">{{ $tm('main.stepNames')[4] }}</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
@@ -64,14 +66,17 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import GraphPanel from '../components/GraphPanel.vue'
|
||||
import Step5Interaction from '../components/Step5Interaction.vue'
|
||||
import { getProject, getGraphData } from '../api/graph'
|
||||
import { getSimulation } from '../api/simulation'
|
||||
import { getReport } from '../api/report'
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
@@ -140,27 +145,27 @@ const toggleMaximize = (target) => {
|
||||
// --- Data Logic ---
|
||||
const loadReportData = async () => {
|
||||
try {
|
||||
addLog(`加载报告数据: ${currentReportId.value}`)
|
||||
|
||||
addLog(t('log.loadReportData', { id: currentReportId.value }))
|
||||
|
||||
// 获取 report 信息以获取 simulation_id
|
||||
const reportRes = await getReport(currentReportId.value)
|
||||
if (reportRes.success && reportRes.data) {
|
||||
const reportData = reportRes.data
|
||||
simulationId.value = reportData.simulation_id
|
||||
|
||||
|
||||
if (simulationId.value) {
|
||||
// 获取 simulation 信息
|
||||
const simRes = await getSimulation(simulationId.value)
|
||||
if (simRes.success && simRes.data) {
|
||||
const simData = simRes.data
|
||||
|
||||
|
||||
// 获取 project 信息
|
||||
if (simData.project_id) {
|
||||
const projRes = await getProject(simData.project_id)
|
||||
if (projRes.success && projRes.data) {
|
||||
projectData.value = projRes.data
|
||||
addLog(`项目加载成功: ${projRes.data.project_id}`)
|
||||
|
||||
addLog(t('log.projectLoadSuccess', { id: projRes.data.project_id }))
|
||||
|
||||
// 获取 graph 数据
|
||||
if (projRes.data.graph_id) {
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
@@ -170,10 +175,10 @@ const loadReportData = async () => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`获取报告信息失败: ${reportRes.error || '未知错误'}`)
|
||||
addLog(t('log.getReportInfoFailed', { error: reportRes.error || t('common.unknownError') }))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(t('log.loadException', { error: err.message }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,10 +189,10 @@ const loadGraph = async (graphId) => {
|
||||
const res = await getGraphData(graphId)
|
||||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
addLog('图谱数据加载成功')
|
||||
addLog(t('log.graphDataLoadSuccess'))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(t('log.graphLoadFailed', { error: err.message }))
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
@@ -208,7 +213,7 @@ watch(() => route.params.reportId, (newId) => {
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
addLog('InteractionView 初始化')
|
||||
addLog(t('log.interactionViewInit'))
|
||||
loadReportData()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -15,15 +15,17 @@
|
||||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: $t('main.layoutGraph'), split: $t('main.layoutSplit'), workbench: $t('main.layoutWorkbench') }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<LanguageSwitcher />
|
||||
<div class="step-divider"></div>
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step {{ currentStep }}/5</span>
|
||||
<span class="step-name">{{ stepNames[currentStep - 1] }}</span>
|
||||
<span class="step-name">{{ $tm('main.stepNames')[currentStep - 1] }}</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
@@ -77,21 +79,24 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } 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 LanguageSwitcher from '../components/LanguageSwitcher.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t, tm } = useI18n()
|
||||
|
||||
// Layout State
|
||||
const viewMode = ref('split') // graph | split | workbench
|
||||
|
||||
// Step State
|
||||
const currentStep = ref(1) // 1: 图谱构建, 2: 环境搭建, 3: 开始模拟, 4: 报告生成, 5: 深度互动
|
||||
const stepNames = ['图谱构建', '环境搭建', '开始模拟', '报告生成', '深度互动']
|
||||
const stepNames = computed(() => tm('main.stepNames'))
|
||||
|
||||
// Data State
|
||||
const currentProjectId = ref(route.params.projectId)
|
||||
@@ -159,11 +164,11 @@ const toggleMaximize = (target) => {
|
||||
const handleNextStep = (params = {}) => {
|
||||
if (currentStep.value < 5) {
|
||||
currentStep.value++
|
||||
addLog(`进入 Step ${currentStep.value}: ${stepNames[currentStep.value - 1]}`)
|
||||
addLog(t('log.enterStep', { step: currentStep.value, name: stepNames.value[currentStep.value - 1] }))
|
||||
|
||||
// 如果是从 Step 2 进入 Step 3,记录模拟轮数配置
|
||||
if (currentStep.value === 3 && params.maxRounds) {
|
||||
addLog(`自定义模拟轮数: ${params.maxRounds} 轮`)
|
||||
addLog(t('log.customSimRounds', { rounds: params.maxRounds }))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -171,7 +176,7 @@ const handleNextStep = (params = {}) => {
|
||||
const handleGoBack = () => {
|
||||
if (currentStep.value > 1) {
|
||||
currentStep.value--
|
||||
addLog(`返回 Step ${currentStep.value}: ${stepNames[currentStep.value - 1]}`)
|
||||
addLog(t('log.returnToStep', { step: currentStep.value, name: stepNames.value[currentStep.value - 1] }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,15 +15,17 @@
|
||||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: $t('main.layoutGraph'), split: $t('main.layoutSplit'), workbench: $t('main.layoutWorkbench') }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<LanguageSwitcher />
|
||||
<div class="step-divider"></div>
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 4/5</span>
|
||||
<span class="step-name">报告生成</span>
|
||||
<span class="step-name">{{ $tm('main.stepNames')[3] }}</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
@@ -64,14 +66,17 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import GraphPanel from '../components/GraphPanel.vue'
|
||||
import Step4Report from '../components/Step4Report.vue'
|
||||
import { getProject, getGraphData } from '../api/graph'
|
||||
import { getSimulation } from '../api/simulation'
|
||||
import { getReport } from '../api/report'
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
@@ -139,27 +144,27 @@ const toggleMaximize = (target) => {
|
||||
// --- Data Logic ---
|
||||
const loadReportData = async () => {
|
||||
try {
|
||||
addLog(`加载报告数据: ${currentReportId.value}`)
|
||||
|
||||
addLog(t('log.loadReportData', { id: currentReportId.value }))
|
||||
|
||||
// 获取 report 信息以获取 simulation_id
|
||||
const reportRes = await getReport(currentReportId.value)
|
||||
if (reportRes.success && reportRes.data) {
|
||||
const reportData = reportRes.data
|
||||
simulationId.value = reportData.simulation_id
|
||||
|
||||
|
||||
if (simulationId.value) {
|
||||
// 获取 simulation 信息
|
||||
const simRes = await getSimulation(simulationId.value)
|
||||
if (simRes.success && simRes.data) {
|
||||
const simData = simRes.data
|
||||
|
||||
|
||||
// 获取 project 信息
|
||||
if (simData.project_id) {
|
||||
const projRes = await getProject(simData.project_id)
|
||||
if (projRes.success && projRes.data) {
|
||||
projectData.value = projRes.data
|
||||
addLog(`项目加载成功: ${projRes.data.project_id}`)
|
||||
|
||||
addLog(t('log.projectLoadSuccess', { id: projRes.data.project_id }))
|
||||
|
||||
// 获取 graph 数据
|
||||
if (projRes.data.graph_id) {
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
@@ -169,10 +174,10 @@ const loadReportData = async () => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`获取报告信息失败: ${reportRes.error || '未知错误'}`)
|
||||
addLog(t('log.getReportInfoFailed', { error: reportRes.error || t('common.unknownError') }))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(t('log.loadException', { error: err.message }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,10 +188,10 @@ const loadGraph = async (graphId) => {
|
||||
const res = await getGraphData(graphId)
|
||||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
addLog('图谱数据加载成功')
|
||||
addLog(t('log.graphDataLoadSuccess'))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(t('log.graphLoadFailed', { error: err.message }))
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
@@ -207,7 +212,7 @@ watch(() => route.params.reportId, (newId) => {
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
addLog('ReportView 初始化')
|
||||
addLog(t('log.reportViewInit'))
|
||||
loadReportData()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -15,15 +15,17 @@
|
||||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: $t('main.layoutGraph'), split: $t('main.layoutSplit'), workbench: $t('main.layoutWorkbench') }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<LanguageSwitcher />
|
||||
<div class="step-divider"></div>
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 3/5</span>
|
||||
<span class="step-name">开始模拟</span>
|
||||
<span class="step-name">{{ $tm('main.stepNames')[2] }}</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
@@ -73,7 +75,10 @@ import GraphPanel from '../components/GraphPanel.vue'
|
||||
import Step3Simulation from '../components/Step3Simulation.vue'
|
||||
import { getProject, getGraphData } from '../api/graph'
|
||||
import { getSimulation, getSimulationConfig, stopSimulation, closeSimulationEnv, getEnvStatus } from '../api/simulation'
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -146,7 +151,7 @@ const toggleMaximize = (target) => {
|
||||
|
||||
const handleGoBack = async () => {
|
||||
// 在返回 Step 2 之前,先关闭正在运行的模拟
|
||||
addLog('准备返回 Step 2,正在关闭模拟...')
|
||||
addLog(t('log.preparingGoBack'))
|
||||
|
||||
// 停止轮询
|
||||
stopGraphRefresh()
|
||||
@@ -156,36 +161,36 @@ const handleGoBack = async () => {
|
||||
const envStatusRes = await getEnvStatus({ simulation_id: currentSimulationId.value })
|
||||
|
||||
if (envStatusRes.success && envStatusRes.data?.env_alive) {
|
||||
addLog('正在关闭模拟环境...')
|
||||
addLog(t('log.closingSimEnv'))
|
||||
try {
|
||||
await closeSimulationEnv({
|
||||
simulation_id: currentSimulationId.value,
|
||||
timeout: 10
|
||||
})
|
||||
addLog('✓ 模拟环境已关闭')
|
||||
addLog(t('log.simEnvClosed'))
|
||||
} catch (closeErr) {
|
||||
addLog(`关闭模拟环境失败,尝试强制停止...`)
|
||||
addLog(t('log.closeSimEnvFailed'))
|
||||
try {
|
||||
await stopSimulation({ simulation_id: currentSimulationId.value })
|
||||
addLog('✓ 模拟已强制停止')
|
||||
addLog(t('log.simForceStopSuccess'))
|
||||
} catch (stopErr) {
|
||||
addLog(`强制停止失败: ${stopErr.message}`)
|
||||
addLog(t('log.forceStopFailed', { error: stopErr.message }))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 环境未运行,检查是否需要停止进程
|
||||
if (isSimulating.value) {
|
||||
addLog('正在停止模拟进程...')
|
||||
addLog(t('log.stoppingSimProcess'))
|
||||
try {
|
||||
await stopSimulation({ simulation_id: currentSimulationId.value })
|
||||
addLog('✓ 模拟已停止')
|
||||
addLog(t('log.simStopped'))
|
||||
} catch (err) {
|
||||
addLog(`停止模拟失败: ${err.message}`)
|
||||
addLog(t('log.stopSimFailed', { error: err.message }))
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`检查模拟状态失败: ${err.message}`)
|
||||
addLog(t('log.checkStatusFailed', { error: err.message }))
|
||||
}
|
||||
|
||||
// 返回到 Step 2 (环境搭建)
|
||||
@@ -195,13 +200,13 @@ const handleGoBack = async () => {
|
||||
const handleNextStep = () => {
|
||||
// Step3Simulation 组件会直接处理报告生成和路由跳转
|
||||
// 这个方法仅作为备用
|
||||
addLog('进入 Step 4: 报告生成')
|
||||
addLog(t('log.enterStep4'))
|
||||
}
|
||||
|
||||
// --- Data Logic ---
|
||||
const loadSimulationData = async () => {
|
||||
try {
|
||||
addLog(`加载模拟数据: ${currentSimulationId.value}`)
|
||||
addLog(t('log.loadingSimData', { id: currentSimulationId.value }))
|
||||
|
||||
// 获取 simulation 信息
|
||||
const simRes = await getSimulation(currentSimulationId.value)
|
||||
@@ -213,10 +218,10 @@ const loadSimulationData = async () => {
|
||||
const configRes = await getSimulationConfig(currentSimulationId.value)
|
||||
if (configRes.success && configRes.data?.time_config?.minutes_per_round) {
|
||||
minutesPerRound.value = configRes.data.time_config.minutes_per_round
|
||||
addLog(`时间配置: 每轮 ${minutesPerRound.value} 分钟`)
|
||||
addLog(t('log.timeConfig', { minutes: minutesPerRound.value }))
|
||||
}
|
||||
} catch (configErr) {
|
||||
addLog(`获取时间配置失败,使用默认值: ${minutesPerRound.value}分钟/轮`)
|
||||
addLog(t('log.timeConfigFetchFailed', { minutes: minutesPerRound.value }))
|
||||
}
|
||||
|
||||
// 获取 project 信息
|
||||
@@ -224,7 +229,7 @@ const loadSimulationData = async () => {
|
||||
const projRes = await getProject(simData.project_id)
|
||||
if (projRes.success && projRes.data) {
|
||||
projectData.value = projRes.data
|
||||
addLog(`项目加载成功: ${projRes.data.project_id}`)
|
||||
addLog(t('log.projectLoadSuccess', { id: projRes.data.project_id }))
|
||||
|
||||
// 获取 graph 数据
|
||||
if (projRes.data.graph_id) {
|
||||
@@ -233,10 +238,10 @@ const loadSimulationData = async () => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`加载模拟数据失败: ${simRes.error || '未知错误'}`)
|
||||
addLog(t('log.loadSimDataFailed', { error: simRes.error || t('common.unknownError') }))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(t('log.loadException', { error: err.message }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,11 +257,11 @@ const loadGraph = async (graphId) => {
|
||||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
if (!isSimulating.value) {
|
||||
addLog('图谱数据加载成功')
|
||||
addLog(t('log.graphDataLoadSuccess'))
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(t('log.graphLoadFailed', { error: err.message }))
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
@@ -273,7 +278,7 @@ let graphRefreshTimer = null
|
||||
|
||||
const startGraphRefresh = () => {
|
||||
if (graphRefreshTimer) return
|
||||
addLog('开启图谱实时刷新 (30s)')
|
||||
addLog(t('log.graphRealtimeRefreshStart'))
|
||||
// 立即刷新一次,然后每30秒刷新
|
||||
graphRefreshTimer = setInterval(refreshGraph, 30000)
|
||||
}
|
||||
@@ -282,7 +287,7 @@ const stopGraphRefresh = () => {
|
||||
if (graphRefreshTimer) {
|
||||
clearInterval(graphRefreshTimer)
|
||||
graphRefreshTimer = null
|
||||
addLog('停止图谱实时刷新')
|
||||
addLog(t('log.graphRealtimeRefreshStop'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,11 +300,11 @@ watch(isSimulating, (newValue) => {
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
addLog('SimulationRunView 初始化')
|
||||
addLog(t('log.simRunViewInit'))
|
||||
|
||||
// 记录 maxRounds 配置(值已在初始化时从 query 参数获取)
|
||||
if (maxRounds.value) {
|
||||
addLog(`自定义模拟轮数: ${maxRounds.value}`)
|
||||
addLog(t('log.customRounds', { rounds: maxRounds.value }))
|
||||
}
|
||||
|
||||
loadSimulationData()
|
||||
|
||||
@@ -15,15 +15,17 @@
|
||||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: $t('main.layoutGraph'), split: $t('main.layoutSplit'), workbench: $t('main.layoutWorkbench') }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-right">
|
||||
<LanguageSwitcher />
|
||||
<div class="step-divider"></div>
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 2/5</span>
|
||||
<span class="step-name">环境搭建</span>
|
||||
<span class="step-name">{{ $tm('main.stepNames')[1] }}</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
@@ -70,7 +72,10 @@ import GraphPanel from '../components/GraphPanel.vue'
|
||||
import Step2EnvSetup from '../components/Step2EnvSetup.vue'
|
||||
import { getProject, getGraphData } from '../api/graph'
|
||||
import { getSimulation, stopSimulation, getEnvStatus, closeSimulationEnv } from '../api/simulation'
|
||||
import LanguageSwitcher from '../components/LanguageSwitcher.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -146,13 +151,13 @@ const handleGoBack = () => {
|
||||
}
|
||||
|
||||
const handleNextStep = (params = {}) => {
|
||||
addLog('进入 Step 3: 开始模拟')
|
||||
|
||||
addLog(t('log.enterStep3'))
|
||||
|
||||
// 记录模拟轮数配置
|
||||
if (params.maxRounds) {
|
||||
addLog(`自定义模拟轮数: ${params.maxRounds} 轮`)
|
||||
addLog(t('log.customRoundsConfig', { rounds: params.maxRounds }))
|
||||
} else {
|
||||
addLog('使用自动配置的模拟轮数')
|
||||
addLog(t('log.useAutoRounds'))
|
||||
}
|
||||
|
||||
// 构建路由参数
|
||||
@@ -184,7 +189,7 @@ const checkAndStopRunningSimulation = async () => {
|
||||
const envStatusRes = await getEnvStatus({ simulation_id: currentSimulationId.value })
|
||||
|
||||
if (envStatusRes.success && envStatusRes.data?.env_alive) {
|
||||
addLog('检测到模拟环境正在运行,正在关闭...')
|
||||
addLog(t('log.detectedSimEnvRunning'))
|
||||
|
||||
// 尝试优雅关闭模拟环境
|
||||
try {
|
||||
@@ -194,14 +199,14 @@ const checkAndStopRunningSimulation = async () => {
|
||||
})
|
||||
|
||||
if (closeRes.success) {
|
||||
addLog('✓ 模拟环境已关闭')
|
||||
addLog(t('log.simEnvClosed'))
|
||||
} else {
|
||||
addLog(`关闭模拟环境失败: ${closeRes.error || '未知错误'}`)
|
||||
addLog(t('log.closeSimEnvFailedWithError', { error: closeRes.error || t('common.unknownError') }))
|
||||
// 如果优雅关闭失败,尝试强制停止
|
||||
await forceStopSimulation()
|
||||
}
|
||||
} catch (closeErr) {
|
||||
addLog(`关闭模拟环境异常: ${closeErr.message}`)
|
||||
addLog(t('log.closeSimEnvException', { error: closeErr.message }))
|
||||
// 如果优雅关闭异常,尝试强制停止
|
||||
await forceStopSimulation()
|
||||
}
|
||||
@@ -209,7 +214,7 @@ const checkAndStopRunningSimulation = async () => {
|
||||
// 环境未运行,但可能进程还在,检查模拟状态
|
||||
const simRes = await getSimulation(currentSimulationId.value)
|
||||
if (simRes.success && simRes.data?.status === 'running') {
|
||||
addLog('检测到模拟状态为运行中,正在停止...')
|
||||
addLog(t('log.detectedSimRunning'))
|
||||
await forceStopSimulation()
|
||||
}
|
||||
}
|
||||
@@ -226,30 +231,30 @@ const forceStopSimulation = async () => {
|
||||
try {
|
||||
const stopRes = await stopSimulation({ simulation_id: currentSimulationId.value })
|
||||
if (stopRes.success) {
|
||||
addLog('✓ 模拟已强制停止')
|
||||
addLog(t('log.simForceStopSuccess'))
|
||||
} else {
|
||||
addLog(`强制停止模拟失败: ${stopRes.error || '未知错误'}`)
|
||||
addLog(t('log.forceStopSimFailed', { error: stopRes.error || t('common.unknownError') }))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`强制停止模拟异常: ${err.message}`)
|
||||
addLog(t('log.forceStopSimException', { error: err.message }))
|
||||
}
|
||||
}
|
||||
|
||||
const loadSimulationData = async () => {
|
||||
try {
|
||||
addLog(`加载模拟数据: ${currentSimulationId.value}`)
|
||||
|
||||
addLog(t('log.loadingSimData', { id: currentSimulationId.value }))
|
||||
|
||||
// 获取 simulation 信息
|
||||
const simRes = await getSimulation(currentSimulationId.value)
|
||||
if (simRes.success && simRes.data) {
|
||||
const simData = simRes.data
|
||||
|
||||
|
||||
// 获取 project 信息
|
||||
if (simData.project_id) {
|
||||
const projRes = await getProject(simData.project_id)
|
||||
if (projRes.success && projRes.data) {
|
||||
projectData.value = projRes.data
|
||||
addLog(`项目加载成功: ${projRes.data.project_id}`)
|
||||
addLog(t('log.projectLoadSuccess', { id: projRes.data.project_id }))
|
||||
|
||||
// 获取 graph 数据
|
||||
if (projRes.data.graph_id) {
|
||||
@@ -258,10 +263,10 @@ const loadSimulationData = async () => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`加载模拟数据失败: ${simRes.error || '未知错误'}`)
|
||||
addLog(t('log.loadSimDataFailed', { error: simRes.error || t('common.unknownError') }))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(t('log.loadException', { error: err.message }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,10 +276,10 @@ const loadGraph = async (graphId) => {
|
||||
const res = await getGraphData(graphId)
|
||||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
addLog('图谱数据加载成功')
|
||||
addLog(t('log.graphDataLoadSuccess'))
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(t('log.graphLoadFailed', { error: err.message }))
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
@@ -287,7 +292,7 @@ const refreshGraph = () => {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
addLog('SimulationView 初始化')
|
||||
addLog(t('log.simViewInit'))
|
||||
|
||||
// 检查并关闭正在运行的模拟(用户从 Step 3 返回时)
|
||||
await checkAndStopRunningSimulation()
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src'),
|
||||
'@locales': path.resolve(__dirname, '../locales')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
open: true,
|
||||
|
||||
Reference in New Issue
Block a user