translate frontend folder
This commit is contained in:
parent
e28c5296f5
commit
e09cfe9985
|
|
@ -7,8 +7,8 @@
|
|||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/icon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="MiroFish - 社交媒体舆论模拟系统" />
|
||||
<title>MiroFish - 预测万物</title>
|
||||
<meta name="description" content="MiroFish - Social Media Public Opinion Simulation System" />
|
||||
<title>MiroFish - Predicting everything</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,11 +3,11 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
// 使用 Vue Router 来管理页面
|
||||
// Sử dụng Vue Router để quản lý trang
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 全局样式重置 */
|
||||
/* Reset style toàn cục */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
/* Style thanh cuộn */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
background: #333333;
|
||||
}
|
||||
|
||||
/* 全局按钮样式 */
|
||||
/* Style button toàn cục */
|
||||
button {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import service, { requestWithRetry } from './index'
|
||||
|
||||
/**
|
||||
* 生成本体(上传文档和模拟需求)
|
||||
* @param {Object} data - 包含files, simulation_requirement, project_name等
|
||||
* Tạo ontology (upload file + yêu cầu mô phỏng)
|
||||
* @param {Object} data - Bao gồm files, simulation_requirement, project_name...
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function generateOntology(formData) {
|
||||
|
|
@ -19,8 +19,8 @@ export function generateOntology(formData) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 构建图谱
|
||||
* @param {Object} data - 包含project_id, graph_name等
|
||||
* Xây dựng graph
|
||||
* @param {Object} data - Bao gồm project_id, graph_name...
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function buildGraph(data) {
|
||||
|
|
@ -34,8 +34,8 @@ export function buildGraph(data) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 查询任务状态
|
||||
* @param {String} taskId - 任务ID
|
||||
* Lấy trạng thái task
|
||||
* @param {String} taskId - ID của task
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getTaskStatus(taskId) {
|
||||
|
|
@ -46,8 +46,8 @@ export function getTaskStatus(taskId) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取图谱数据
|
||||
* @param {String} graphId - 图谱ID
|
||||
* Lấy dữ liệu graph
|
||||
* @param {String} graphId - ID của graph
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getGraphData(graphId) {
|
||||
|
|
@ -58,8 +58,8 @@ export function getGraphData(graphId) {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取项目信息
|
||||
* @param {String} projectId - 项目ID
|
||||
* Lấy thông tin project
|
||||
* @param {String} projectId - ID của project
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getProject(projectId) {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
// Tạo instance axios
|
||||
const service = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5001',
|
||||
timeout: 300000, // 5分钟超时(本体生成可能需要较长时间)
|
||||
timeout: 300000, // Thời gian timeout 5 phút (việc tạo nội dung có thể mất nhiều thời gian)
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
// Interceptor request
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
return config
|
||||
|
|
@ -20,12 +20,12 @@ service.interceptors.request.use(
|
|||
}
|
||||
)
|
||||
|
||||
// 响应拦截器(容错重试机制)
|
||||
// Interceptor response (cơ chế retry chịu lỗi)
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
// 如果返回的状态码不是success,则抛出错误
|
||||
// Nếu mã trạng thái trả về không phải success thì ném lỗi
|
||||
if (!res.success && res.success !== undefined) {
|
||||
console.error('API Error:', res.error || res.message || 'Unknown error')
|
||||
return Promise.reject(new Error(res.error || res.message || 'Error'))
|
||||
|
|
@ -36,12 +36,12 @@ service.interceptors.response.use(
|
|||
error => {
|
||||
console.error('Response error:', error)
|
||||
|
||||
// 处理超时
|
||||
// Xử lý timeout
|
||||
if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
|
||||
console.error('Request timeout')
|
||||
}
|
||||
|
||||
// 处理网络错误
|
||||
// Xử lý lỗi mạng
|
||||
if (error.message === 'Network Error') {
|
||||
console.error('Network error - please check your connection')
|
||||
}
|
||||
|
|
@ -50,7 +50,7 @@ service.interceptors.response.use(
|
|||
}
|
||||
)
|
||||
|
||||
// 带重试的请求函数
|
||||
// Hàm request có retry
|
||||
export const requestWithRetry = async (requestFn, maxRetries = 3, delay = 1000) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import service, { requestWithRetry } from './index'
|
||||
|
||||
/**
|
||||
* 开始报告生成
|
||||
* Bắt đầu tạo báo cáo
|
||||
* @param {Object} data - { simulation_id, force_regenerate? }
|
||||
*/
|
||||
export const generateReport = (data) => {
|
||||
|
|
@ -9,7 +9,7 @@ export const generateReport = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取报告生成状态
|
||||
* Lấy trạng thái tạo báo cáo
|
||||
* @param {string} reportId
|
||||
*/
|
||||
export const getReportStatus = (reportId) => {
|
||||
|
|
@ -17,25 +17,25 @@ export const getReportStatus = (reportId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 日志(增量)
|
||||
* Lấy log Agent (dạng tăng dần)
|
||||
* @param {string} reportId
|
||||
* @param {number} fromLine - 从第几行开始获取
|
||||
* @param {number} fromLine - Lấy từ dòng thứ bao nhiêu
|
||||
*/
|
||||
export const getAgentLog = (reportId, fromLine = 0) => {
|
||||
return service.get(`/api/report/${reportId}/agent-log`, { params: { from_line: fromLine } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取控制台日志(增量)
|
||||
* Lấy log console (dạng tăng dần)
|
||||
* @param {string} reportId
|
||||
* @param {number} fromLine - 从第几行开始获取
|
||||
* @param {number} fromLine - Lấy từ dòng thứ bao nhiêu
|
||||
*/
|
||||
export const getConsoleLog = (reportId, fromLine = 0) => {
|
||||
return service.get(`/api/report/${reportId}/console-log`, { params: { from_line: fromLine } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取报告详情
|
||||
* Lấy chi tiết báo cáo
|
||||
* @param {string} reportId
|
||||
*/
|
||||
export const getReport = (reportId) => {
|
||||
|
|
@ -43,7 +43,7 @@ export const getReport = (reportId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 与 Report Agent 对话
|
||||
* Chat với Report Agent
|
||||
* @param {Object} data - { simulation_id, message, chat_history? }
|
||||
*/
|
||||
export const chatWithReport = (data) => {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import service, { requestWithRetry } from './index'
|
||||
|
||||
/**
|
||||
* 创建模拟
|
||||
* Tạo simulation
|
||||
* @param {Object} data - { project_id, graph_id?, enable_twitter?, enable_reddit? }
|
||||
*/
|
||||
export const createSimulation = (data) => {
|
||||
|
|
@ -9,7 +9,7 @@ export const createSimulation = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 准备模拟环境(异步任务)
|
||||
* Chuẩn bị môi trường simulation (task bất đồng bộ)
|
||||
* @param {Object} data - { simulation_id, entity_types?, use_llm_for_profiles?, parallel_profile_count?, force_regenerate? }
|
||||
*/
|
||||
export const prepareSimulation = (data) => {
|
||||
|
|
@ -17,7 +17,7 @@ export const prepareSimulation = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 查询准备任务进度
|
||||
* Kiểm tra tiến độ task chuẩn bị
|
||||
* @param {Object} data - { task_id?, simulation_id? }
|
||||
*/
|
||||
export const getPrepareStatus = (data) => {
|
||||
|
|
@ -25,7 +25,7 @@ export const getPrepareStatus = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟状态
|
||||
* Lấy trạng thái simulation
|
||||
* @param {string} simulationId
|
||||
*/
|
||||
export const getSimulation = (simulationId) => {
|
||||
|
|
@ -33,7 +33,7 @@ export const getSimulation = (simulationId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟的 Agent Profiles
|
||||
* Lấy Agent Profiles của simulation
|
||||
* @param {string} simulationId
|
||||
* @param {string} platform - 'reddit' | 'twitter'
|
||||
*/
|
||||
|
|
@ -42,7 +42,7 @@ export const getSimulationProfiles = (simulationId, platform = 'reddit') => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 实时获取生成中的 Agent Profiles
|
||||
* Lấy Agent Profiles đang được tạo (realtime)
|
||||
* @param {string} simulationId
|
||||
* @param {string} platform - 'reddit' | 'twitter'
|
||||
*/
|
||||
|
|
@ -51,7 +51,7 @@ export const getSimulationProfilesRealtime = (simulationId, platform = 'reddit')
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟配置
|
||||
* Lấy cấu hình simulation
|
||||
* @param {string} simulationId
|
||||
*/
|
||||
export const getSimulationConfig = (simulationId) => {
|
||||
|
|
@ -59,17 +59,17 @@ export const getSimulationConfig = (simulationId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 实时获取生成中的模拟配置
|
||||
* Lấy cấu hình simulation đang được tạo (realtime)
|
||||
* @param {string} simulationId
|
||||
* @returns {Promise} 返回配置信息,包含元数据和配置内容
|
||||
* @returns {Promise} Trả về thông tin cấu hình, bao gồm metadata và nội dung cấu hình
|
||||
*/
|
||||
export const getSimulationConfigRealtime = (simulationId) => {
|
||||
return service.get(`/api/simulation/${simulationId}/config/realtime`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有模拟
|
||||
* @param {string} projectId - 可选,按项目ID过滤
|
||||
* Liệt kê tất cả simulation
|
||||
* @param {string} projectId - Tùy chọn, lọc theo project ID
|
||||
*/
|
||||
export const listSimulations = (projectId) => {
|
||||
const params = projectId ? { project_id: projectId } : {}
|
||||
|
|
@ -77,7 +77,7 @@ export const listSimulations = (projectId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 启动模拟
|
||||
* Bắt đầu simulation
|
||||
* @param {Object} data - { simulation_id, platform?, max_rounds?, enable_graph_memory_update? }
|
||||
*/
|
||||
export const startSimulation = (data) => {
|
||||
|
|
@ -85,7 +85,7 @@ export const startSimulation = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 停止模拟
|
||||
* Dừng simulation
|
||||
* @param {Object} data - { simulation_id }
|
||||
*/
|
||||
export const stopSimulation = (data) => {
|
||||
|
|
@ -93,7 +93,7 @@ export const stopSimulation = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟运行实时状态
|
||||
* Lấy trạng thái chạy realtime của simulation
|
||||
* @param {string} simulationId
|
||||
*/
|
||||
export const getRunStatus = (simulationId) => {
|
||||
|
|
@ -101,7 +101,7 @@ export const getRunStatus = (simulationId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟运行详细状态(包含最近动作)
|
||||
* Lấy trạng thái chạy chi tiết (bao gồm hành động gần nhất)
|
||||
* @param {string} simulationId
|
||||
*/
|
||||
export const getRunStatusDetail = (simulationId) => {
|
||||
|
|
@ -109,11 +109,11 @@ export const getRunStatusDetail = (simulationId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟中的帖子
|
||||
* Lấy danh sách bài post trong simulation
|
||||
* @param {string} simulationId
|
||||
* @param {string} platform - 'reddit' | 'twitter'
|
||||
* @param {number} limit - 返回数量
|
||||
* @param {number} offset - 偏移量
|
||||
* @param {number} limit - Số lượng trả về
|
||||
* @param {number} offset - Độ lệch
|
||||
*/
|
||||
export const getSimulationPosts = (simulationId, platform = 'reddit', limit = 50, offset = 0) => {
|
||||
return service.get(`/api/simulation/${simulationId}/posts`, {
|
||||
|
|
@ -122,10 +122,10 @@ export const getSimulationPosts = (simulationId, platform = 'reddit', limit = 50
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟时间线(按轮次汇总)
|
||||
* Lấy timeline simulation (tổng hợp theo từng round)
|
||||
* @param {string} simulationId
|
||||
* @param {number} startRound - 起始轮次
|
||||
* @param {number} endRound - 结束轮次
|
||||
* @param {number} startRound - Round bắt đầu
|
||||
* @param {number} endRound - Round kết thúc
|
||||
*/
|
||||
export const getSimulationTimeline = (simulationId, startRound = 0, endRound = null) => {
|
||||
const params = { start_round: startRound }
|
||||
|
|
@ -136,7 +136,7 @@ export const getSimulationTimeline = (simulationId, startRound = 0, endRound = n
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取Agent统计信息
|
||||
* Lấy thống kê Agent
|
||||
* @param {string} simulationId
|
||||
*/
|
||||
export const getAgentStats = (simulationId) => {
|
||||
|
|
@ -144,7 +144,7 @@ export const getAgentStats = (simulationId) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟动作历史
|
||||
* Lấy lịch sử hành động simulation
|
||||
* @param {string} simulationId
|
||||
* @param {Object} params - { limit, offset, platform, agent_id, round_num }
|
||||
*/
|
||||
|
|
@ -153,7 +153,7 @@ export const getSimulationActions = (simulationId, params = {}) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 关闭模拟环境(优雅退出)
|
||||
* Đóng môi trường simulation (thoát an toàn)
|
||||
* @param {Object} data - { simulation_id, timeout? }
|
||||
*/
|
||||
export const closeSimulationEnv = (data) => {
|
||||
|
|
@ -161,7 +161,7 @@ export const closeSimulationEnv = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取模拟环境状态
|
||||
* Lấy trạng thái môi trường simulation
|
||||
* @param {Object} data - { simulation_id }
|
||||
*/
|
||||
export const getEnvStatus = (data) => {
|
||||
|
|
@ -169,7 +169,7 @@ export const getEnvStatus = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 批量采访 Agent
|
||||
* Phỏng vấn hàng loạt Agent
|
||||
* @param {Object} data - { simulation_id, interviews: [{ agent_id, prompt }] }
|
||||
*/
|
||||
export const interviewAgents = (data) => {
|
||||
|
|
@ -177,11 +177,10 @@ export const interviewAgents = (data) => {
|
|||
}
|
||||
|
||||
/**
|
||||
* 获取历史模拟列表(带项目详情)
|
||||
* 用于首页历史项目展示
|
||||
* @param {number} limit - 返回数量限制
|
||||
* Lấy lịch sử simulation (kèm thông tin project)
|
||||
* Dùng cho hiển thị trang chủ
|
||||
* @param {number} limit - Giới hạn số lượng trả về
|
||||
*/
|
||||
export const getSimulationHistory = (limit = 20) => {
|
||||
return service.get('/api/simulation/history', { params: { limit } })
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,24 +2,24 @@
|
|||
<div class="graph-panel">
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">Graph Relationship Visualization</span>
|
||||
<!-- 顶部工具栏 (Internal Top Right) -->
|
||||
<!-- Thanh công cụ phía trên (bên phải nội bộ) -->
|
||||
<div class="header-tools">
|
||||
<button class="tool-btn" @click="$emit('refresh')" :disabled="loading" title="刷新图谱">
|
||||
<button class="tool-btn" @click="$emit('refresh')" :disabled="loading" title="Refresh graph">
|
||||
<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="Maximize/Restore">
|
||||
<span class="icon-maximize">⛶</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="graph-container" ref="graphContainer">
|
||||
<!-- 图谱可视化 -->
|
||||
<!-- Visualization of graph -->
|
||||
<div v-if="graphData" class="graph-view">
|
||||
<svg ref="graphSvg" class="graph-svg"></svg>
|
||||
|
||||
<!-- 构建中/模拟中提示 -->
|
||||
<!-- Hint when building/simulating -->
|
||||
<div v-if="currentPhase === 1 || isSimulating" class="graph-building-hint">
|
||||
<div class="memory-icon-wrapper">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="memory-icon">
|
||||
|
|
@ -27,10 +27,10 @@
|
|||
<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 ? 'GraphRAG long-term and short-term memory updating in real time' : 'Updating in real time...' }}
|
||||
</div>
|
||||
|
||||
<!-- 模拟结束后的提示 -->
|
||||
<!-- Hint after simulation finished -->
|
||||
<div v-if="showSimulationFinishedHint" class="graph-building-hint finished-hint">
|
||||
<div class="hint-icon-wrapper">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="hint-icon">
|
||||
|
|
@ -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">Some content is still being processed, it is recommended to manually refresh the graph later</span>
|
||||
<button class="hint-close-btn" @click="dismissFinishedHint" title="Close hint">
|
||||
<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>
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 节点/边详情面板 -->
|
||||
<!-- Panel chi tiết node/cạnh -->
|
||||
<div v-if="selectedItem" class="detail-panel">
|
||||
<div class="detail-panel-header">
|
||||
<span class="detail-title">{{ selectedItem.type === 'node' ? 'Node Details' : 'Relationship' }}</span>
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
<button class="detail-close" @click="closeDetailPanel">×</button>
|
||||
</div>
|
||||
|
||||
<!-- 节点详情 -->
|
||||
<!-- Chi tiết node -->
|
||||
<div v-if="selectedItem.type === 'node'" class="detail-content">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Name:</span>
|
||||
|
|
@ -101,9 +101,9 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 边详情 -->
|
||||
<!-- Chi tiết cạnh -->
|
||||
<div v-else class="detail-content">
|
||||
<!-- 自环组详情 -->
|
||||
<!-- Chi tiết nhóm self-loop -->
|
||||
<template v-if="selectedItem.data.isSelfLoopGroup">
|
||||
<div class="edge-relation-header self-loop-header">
|
||||
{{ selectedItem.data.source_name }} - Self Relations
|
||||
|
|
@ -154,7 +154,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 普通边详情 -->
|
||||
<!-- Chi tiết cạnh thông thường -->
|
||||
<template v-else>
|
||||
<div class="edge-relation-header">
|
||||
{{ selectedItem.data.source_name }} → {{ selectedItem.data.name || 'RELATED_TO' }} → {{ selectedItem.data.target_name }}
|
||||
|
|
@ -200,20 +200,20 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<!-- Trạng thái loading -->
|
||||
<div v-else-if="loading" class="graph-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>图谱数据加载中...</p>
|
||||
<p>Graph data is loading...</p>
|
||||
</div>
|
||||
|
||||
<!-- 等待/空状态 -->
|
||||
<!-- Trạng thái chờ/rỗng -->
|
||||
<div v-else class="graph-state">
|
||||
<div class="empty-icon">❖</div>
|
||||
<p class="empty-text">等待本体生成...</p>
|
||||
<p class="empty-text">Waiting for ontology generation...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部图例 (Bottom Left) -->
|
||||
<!-- Chú giải phía dưới (bên trái) -->
|
||||
<div v-if="graphData && entityTypes.length" class="graph-legend">
|
||||
<span class="legend-title">Entity Types</span>
|
||||
<div class="legend-items">
|
||||
|
|
@ -224,7 +224,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示边标签开关 -->
|
||||
<!-- Công tắc hiển thị nhãn cạnh -->
|
||||
<div v-if="graphData" class="edge-labels-toggle">
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" v-model="showEdgeLabels" />
|
||||
|
|
@ -251,26 +251,26 @@ const emit = defineEmits(['refresh', 'toggle-maximize'])
|
|||
const graphContainer = ref(null)
|
||||
const graphSvg = ref(null)
|
||||
const selectedItem = ref(null)
|
||||
const showEdgeLabels = ref(true) // 默认显示边标签
|
||||
const expandedSelfLoops = ref(new Set()) // 展开的自环项
|
||||
const showSimulationFinishedHint = ref(false) // 模拟结束后的提示
|
||||
const wasSimulating = ref(false) // 追踪之前是否在模拟中
|
||||
const showEdgeLabels = ref(true) // Mặc định hiển thị nhãn cạnh
|
||||
const expandedSelfLoops = ref(new Set()) // Các self-loop đang mở
|
||||
const showSimulationFinishedHint = ref(false) // Thông báo sau khi mô phỏng kết thúc
|
||||
const wasSimulating = ref(false) // Theo dõi trước đó có đang mô phỏng hay không
|
||||
|
||||
// 关闭模拟结束提示
|
||||
// Đóng thông báo kết thúc mô phỏng
|
||||
const dismissFinishedHint = () => {
|
||||
showSimulationFinishedHint.value = false
|
||||
}
|
||||
|
||||
// 监听 isSimulating 变化,检测模拟结束
|
||||
// Theo dõi thay đổi isSimulating để phát hiện kết thúc mô phỏng
|
||||
watch(() => props.isSimulating, (newValue, oldValue) => {
|
||||
if (wasSimulating.value && !newValue) {
|
||||
// 从模拟中变为非模拟状态,显示结束提示
|
||||
// Từ trạng thái mô phỏng chuyển sang không mô phỏng, hiển thị thông báo kết thúc
|
||||
showSimulationFinishedHint.value = true
|
||||
}
|
||||
wasSimulating.value = newValue
|
||||
}, { immediate: true })
|
||||
|
||||
// 切换自环项展开/折叠状态
|
||||
// Toggle trạng thái mở/đóng của self-loop
|
||||
const toggleSelfLoop = (id) => {
|
||||
const newSet = new Set(expandedSelfLoops.value)
|
||||
if (newSet.has(id)) {
|
||||
|
|
@ -281,11 +281,11 @@ const toggleSelfLoop = (id) => {
|
|||
expandedSelfLoops.value = newSet
|
||||
}
|
||||
|
||||
// 计算实体类型用于图例
|
||||
// Tính toán loại entity để hiển thị legend
|
||||
const entityTypes = computed(() => {
|
||||
if (!props.graphData?.nodes) return []
|
||||
const typeMap = {}
|
||||
// 美观的颜色调色板
|
||||
// Bảng màu đẹp
|
||||
const colors = ['#FF6B35', '#004E89', '#7B2D8E', '#1A936F', '#C5283D', '#E9724C', '#3498db', '#9b59b6', '#27ae60', '#f39c12']
|
||||
|
||||
props.graphData.nodes.forEach(node => {
|
||||
|
|
@ -298,7 +298,7 @@ const entityTypes = computed(() => {
|
|||
return Object.values(typeMap)
|
||||
})
|
||||
|
||||
// 格式化时间
|
||||
// Format thời gian
|
||||
const formatDateTime = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
|
|
@ -318,7 +318,7 @@ const formatDateTime = (dateStr) => {
|
|||
|
||||
const closeDetailPanel = () => {
|
||||
selectedItem.value = null
|
||||
expandedSelfLoops.value = new Set() // 重置展开状态
|
||||
expandedSelfLoops.value = new Set() // Reset trạng thái mở
|
||||
}
|
||||
|
||||
let currentSimulation = null
|
||||
|
|
@ -328,7 +328,7 @@ let linkLabelBgRef = null
|
|||
const renderGraph = () => {
|
||||
if (!graphSvg.value || !props.graphData) return
|
||||
|
||||
// 停止之前的仿真
|
||||
// Dừng simulation trước đó
|
||||
if (currentSimulation) {
|
||||
currentSimulation.stop()
|
||||
}
|
||||
|
|
@ -349,7 +349,7 @@ const renderGraph = () => {
|
|||
|
||||
if (nodesData.length === 0) return
|
||||
|
||||
// Prep data
|
||||
// Chuẩn bị dữ liệu
|
||||
const nodeMap = {}
|
||||
nodesData.forEach(n => nodeMap[n.uuid] = n)
|
||||
|
||||
|
|
@ -362,16 +362,16 @@ const renderGraph = () => {
|
|||
|
||||
const nodeIds = new Set(nodes.map(n => n.id))
|
||||
|
||||
// 处理边数据,计算同一对节点间的边数量和索引
|
||||
// Xử lý dữ liệu cạnh, tính số lượng và chỉ số cạnh giữa cùng một cặp node
|
||||
const edgePairCount = {}
|
||||
const selfLoopEdges = {} // 按节点分组的自环边
|
||||
const selfLoopEdges = {} // Self-loop được nhóm theo node
|
||||
const tempEdges = edgesData
|
||||
.filter(e => nodeIds.has(e.source_node_uuid) && nodeIds.has(e.target_node_uuid))
|
||||
|
||||
// 统计每对节点之间的边数量,收集自环边
|
||||
// Thống kê số cạnh giữa mỗi cặp node, thu thập self-loop
|
||||
tempEdges.forEach(e => {
|
||||
if (e.source_node_uuid === e.target_node_uuid) {
|
||||
// 自环 - 收集到数组中
|
||||
// Self-loop - gom vào mảng
|
||||
if (!selfLoopEdges[e.source_node_uuid]) {
|
||||
selfLoopEdges[e.source_node_uuid] = []
|
||||
}
|
||||
|
|
@ -386,9 +386,9 @@ const renderGraph = () => {
|
|||
}
|
||||
})
|
||||
|
||||
// 记录当前处理到每对节点的第几条边
|
||||
// Ghi nhận đang xử lý cạnh thứ mấy của mỗi cặp node
|
||||
const edgePairIndex = {}
|
||||
const processedSelfLoopNodes = new Set() // 已处理的自环节点
|
||||
const processedSelfLoopNodes = new Set() // Các node self-loop đã xử lý
|
||||
|
||||
const edges = []
|
||||
|
||||
|
|
@ -396,9 +396,9 @@ const renderGraph = () => {
|
|||
const isSelfLoop = e.source_node_uuid === e.target_node_uuid
|
||||
|
||||
if (isSelfLoop) {
|
||||
// 自环边 - 每个节点只添加一条合并的自环
|
||||
// Self-loop - mỗi node chỉ thêm một self-loop tổng hợp
|
||||
if (processedSelfLoopNodes.has(e.source_node_uuid)) {
|
||||
return // 已处理过,跳过
|
||||
return // Đã xử lý rồi, bỏ qua
|
||||
}
|
||||
processedSelfLoopNodes.add(e.source_node_uuid)
|
||||
|
||||
|
|
@ -417,7 +417,7 @@ const renderGraph = () => {
|
|||
source_name: nodeName,
|
||||
target_name: nodeName,
|
||||
selfLoopCount: allSelfLoops.length,
|
||||
selfLoopEdges: allSelfLoops // 存储所有自环边的详细信息
|
||||
selfLoopEdges: allSelfLoops // Lưu toàn bộ thông tin chi tiết của self-loop
|
||||
}
|
||||
})
|
||||
return
|
||||
|
|
@ -428,19 +428,19 @@ const renderGraph = () => {
|
|||
const currentIndex = edgePairIndex[pairKey] || 0
|
||||
edgePairIndex[pairKey] = currentIndex + 1
|
||||
|
||||
// 判断边的方向是否与标准化方向一致(源UUID < 目标UUID)
|
||||
// Kiểm tra hướng cạnh có trùng với hướng chuẩn hay không (source UUID < target UUID)
|
||||
const isReversed = e.source_node_uuid > e.target_node_uuid
|
||||
|
||||
// 计算曲率:多条边时分散开,单条边为直线
|
||||
// Tính độ cong: nhiều cạnh thì tách ra, một cạnh thì là đường thẳng
|
||||
let curvature = 0
|
||||
if (totalCount > 1) {
|
||||
// 均匀分布曲率,确保明显区分
|
||||
// 曲率范围根据边数量增加,边越多曲率范围越大
|
||||
// Phân bố độ cong đồng đều, đảm bảo dễ phân biệt
|
||||
// Phạm vi độ cong tăng theo số lượng cạnh, càng nhiều cạnh thì phạm vi càng lớn
|
||||
const curvatureRange = Math.min(1.2, 0.6 + totalCount * 0.15)
|
||||
curvature = ((currentIndex / (totalCount - 1)) - 0.5) * curvatureRange * 2
|
||||
|
||||
// 如果边的方向与标准化方向相反,翻转曲率
|
||||
// 这样确保所有边在同一参考系下分布,不会因方向不同而重叠
|
||||
// Nếu hướng cạnh ngược với hướng chuẩn, đảo độ cong
|
||||
// Nhờ vậy tất cả cạnh được phân bố trong cùng hệ quy chiếu, tránh chồng lấn do khác hướng
|
||||
if (isReversed) {
|
||||
curvature = -curvature
|
||||
}
|
||||
|
|
@ -463,16 +463,16 @@ const renderGraph = () => {
|
|||
})
|
||||
})
|
||||
|
||||
// Color scale
|
||||
// Thang màu
|
||||
const colorMap = {}
|
||||
entityTypes.value.forEach(t => colorMap[t.name] = t.color)
|
||||
const getColor = (type) => colorMap[type] || '#999'
|
||||
|
||||
// Simulation - 根据边数量动态调整节点间距
|
||||
// Simulation - điều chỉnh khoảng cách node theo số lượng cạnh
|
||||
const simulation = d3.forceSimulation(nodes)
|
||||
.force('link', d3.forceLink(edges).id(d => d.id).distance(d => {
|
||||
// 根据这对节点之间的边数量动态调整距离
|
||||
// 基础距离 150,每多一条边增加 40
|
||||
// Điều chỉnh khoảng cách dựa trên số cạnh giữa cặp node này
|
||||
// Khoảng cách cơ bản là 150, mỗi cạnh thêm tăng 50
|
||||
const baseDistance = 150
|
||||
const edgeCount = d.pairTotal || 1
|
||||
return baseDistance + (edgeCount - 1) * 50
|
||||
|
|
@ -480,7 +480,7 @@ const renderGraph = () => {
|
|||
.force('charge', d3.forceManyBody().strength(-400))
|
||||
.force('center', d3.forceCenter(width / 2, height / 2))
|
||||
.force('collide', d3.forceCollide(50))
|
||||
// 添加向中心的引力,让独立的节点群聚集到中心区域
|
||||
// Thêm lực hút về trung tâm để các cụm node độc lập gom lại gần trung tâm
|
||||
.force('x', d3.forceX(width / 2).strength(0.04))
|
||||
.force('y', d3.forceY(height / 2).strength(0.04))
|
||||
|
||||
|
|
@ -488,44 +488,44 @@ const renderGraph = () => {
|
|||
|
||||
const g = svg.append('g')
|
||||
|
||||
// Zoom
|
||||
// Thu phóng
|
||||
svg.call(d3.zoom().extent([[0, 0], [width, height]]).scaleExtent([0.1, 4]).on('zoom', (event) => {
|
||||
g.attr('transform', event.transform)
|
||||
}))
|
||||
|
||||
// Links - 使用 path 支持曲线
|
||||
// Links - dùng path để hỗ trợ đường cong
|
||||
const linkGroup = g.append('g').attr('class', 'links')
|
||||
|
||||
// 计算曲线路径
|
||||
// Tính đường dẫn cho đường cong
|
||||
const getLinkPath = (d) => {
|
||||
const sx = d.source.x, sy = d.source.y
|
||||
const tx = d.target.x, ty = d.target.y
|
||||
|
||||
// 检测自环
|
||||
// Kiểm tra self-loop
|
||||
if (d.isSelfLoop) {
|
||||
// 自环:绘制一个圆弧从节点出发再返回
|
||||
// Self-loop: vẽ một cung tròn đi ra từ node rồi quay lại
|
||||
const loopRadius = 30
|
||||
// 从节点右侧出发,绕一圈回来
|
||||
const x1 = sx + 8 // 起点偏移
|
||||
// Bắt đầu từ bên phải node, đi một vòng rồi quay lại
|
||||
const x1 = sx + 8
|
||||
const y1 = sy - 4
|
||||
const x2 = sx + 8 // 终点偏移
|
||||
const x2 = sx + 8
|
||||
const y2 = sy + 4
|
||||
// 使用圆弧绘制自环(sweep-flag=1 顺时针)
|
||||
// Dùng cung tròn để vẽ self-loop (sweep-flag=1 theo chiều kim đồng hồ)
|
||||
return `M${x1},${y1} A${loopRadius},${loopRadius} 0 1,1 ${x2},${y2}`
|
||||
}
|
||||
|
||||
if (d.curvature === 0) {
|
||||
// 直线
|
||||
// Đường thẳng
|
||||
return `M${sx},${sy} L${tx},${ty}`
|
||||
}
|
||||
|
||||
// 计算曲线控制点 - 根据边数量和距离动态调整
|
||||
// Tính điểm điều khiển của đường cong - điều chỉnh động theo số cạnh và khoảng cách
|
||||
const dx = tx - sx, dy = ty - sy
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
// 垂直于连线方向的偏移,根据距离比例计算,保证曲线明显可见
|
||||
// 边越多,偏移量占距离的比例越大
|
||||
// Độ lệch vuông góc với hướng nối, tính theo tỉ lệ khoảng cách để đảm bảo nhìn rõ
|
||||
// Càng nhiều cạnh, tỉ lệ độ lệch theo khoảng cách càng lớn
|
||||
const pairTotal = d.pairTotal || 1
|
||||
const offsetRatio = 0.25 + pairTotal * 0.05 // 基础25%,每多一条边增加5%
|
||||
const offsetRatio = 0.25 + pairTotal * 0.05 // Giá trị cơ bản là 25%, và nó tăng thêm 5% cho mỗi cạnh bổ sung.
|
||||
const baseOffset = Math.max(35, dist * offsetRatio)
|
||||
const offsetX = -dy / dist * d.curvature * baseOffset
|
||||
const offsetY = dx / dist * d.curvature * baseOffset
|
||||
|
|
@ -535,14 +535,14 @@ const renderGraph = () => {
|
|||
return `M${sx},${sy} Q${cx},${cy} ${tx},${ty}`
|
||||
}
|
||||
|
||||
// 计算曲线中点(用于标签定位)
|
||||
// Tính trung điểm của đường cong (để đặt label)
|
||||
const getLinkMidpoint = (d) => {
|
||||
const sx = d.source.x, sy = d.source.y
|
||||
const tx = d.target.x, ty = d.target.y
|
||||
|
||||
// 检测自环
|
||||
// Kiểm tra self-loop
|
||||
if (d.isSelfLoop) {
|
||||
// 自环标签位置:节点右侧
|
||||
// Vị trí label self-loop: bên phải node
|
||||
return { x: sx + 70, y: sy }
|
||||
}
|
||||
|
||||
|
|
@ -550,7 +550,7 @@ const renderGraph = () => {
|
|||
return { x: (sx + tx) / 2, y: (sy + ty) / 2 }
|
||||
}
|
||||
|
||||
// 二次贝塞尔曲线的中点 t=0.5
|
||||
// Trung điểm của đường Bezier bậc hai tại t=0.5
|
||||
const dx = tx - sx, dy = ty - sy
|
||||
const dist = Math.sqrt(dx * dx + dy * dy)
|
||||
const pairTotal = d.pairTotal || 1
|
||||
|
|
@ -561,7 +561,7 @@ const renderGraph = () => {
|
|||
const cx = (sx + tx) / 2 + offsetX
|
||||
const cy = (sy + ty) / 2 + offsetY
|
||||
|
||||
// 二次贝塞尔曲线公式 B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2, t=0.5
|
||||
// Công thức Bezier bậc hai B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2, với t=0.5
|
||||
const midX = 0.25 * sx + 0.5 * cx + 0.25 * tx
|
||||
const midY = 0.25 * sy + 0.5 * cy + 0.25 * ty
|
||||
|
||||
|
|
@ -577,11 +577,11 @@ const renderGraph = () => {
|
|||
.style('cursor', 'pointer')
|
||||
.on('click', (event, d) => {
|
||||
event.stopPropagation()
|
||||
// 重置之前选中边的样式
|
||||
// Reset style cạnh đã chọn trước đó
|
||||
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
|
||||
linkLabelBg.attr('fill', 'rgba(255,255,255,0.95)')
|
||||
linkLabels.attr('fill', '#666')
|
||||
// 高亮当前选中的边
|
||||
// Highlight cạnh đang chọn
|
||||
d3.select(event.target).attr('stroke', '#3498db').attr('stroke-width', 3)
|
||||
|
||||
selectedItem.value = {
|
||||
|
|
@ -590,7 +590,7 @@ const renderGraph = () => {
|
|||
}
|
||||
})
|
||||
|
||||
// Link labels background (白色背景使文字更清晰)
|
||||
// Nền label của cạnh (nền trắng giúp chữ rõ hơn)
|
||||
const linkLabelBg = linkGroup.selectAll('rect')
|
||||
.data(edges)
|
||||
.enter().append('rect')
|
||||
|
|
@ -605,7 +605,7 @@ const renderGraph = () => {
|
|||
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
|
||||
linkLabelBg.attr('fill', 'rgba(255,255,255,0.95)')
|
||||
linkLabels.attr('fill', '#666')
|
||||
// 高亮对应的边
|
||||
// Highlight cạnh tương ứng
|
||||
link.filter(l => l === d).attr('stroke', '#3498db').attr('stroke-width', 3)
|
||||
d3.select(event.target).attr('fill', 'rgba(52, 152, 219, 0.1)')
|
||||
|
||||
|
|
@ -633,7 +633,7 @@ const renderGraph = () => {
|
|||
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
|
||||
linkLabelBg.attr('fill', 'rgba(255,255,255,0.95)')
|
||||
linkLabels.attr('fill', '#666')
|
||||
// 高亮对应的边
|
||||
// Highlight cạnh tương ứng
|
||||
link.filter(l => l === d).attr('stroke', '#3498db').attr('stroke-width', 3)
|
||||
d3.select(event.target).attr('fill', '#3498db')
|
||||
|
||||
|
|
@ -643,14 +643,14 @@ const renderGraph = () => {
|
|||
}
|
||||
})
|
||||
|
||||
// 保存引用供外部控制显隐
|
||||
// Lưu reference để điều khiển hiển thị từ bên ngoài
|
||||
linkLabelsRef = linkLabels
|
||||
linkLabelBgRef = linkLabelBg
|
||||
|
||||
// Nodes group
|
||||
// Nhóm node
|
||||
const nodeGroup = g.append('g').attr('class', 'nodes')
|
||||
|
||||
// Node circles
|
||||
// Hình tròn node
|
||||
const node = nodeGroup.selectAll('circle')
|
||||
.data(nodes)
|
||||
.enter().append('circle')
|
||||
|
|
@ -661,7 +661,7 @@ const renderGraph = () => {
|
|||
.style('cursor', 'pointer')
|
||||
.call(d3.drag()
|
||||
.on('start', (event, d) => {
|
||||
// 只记录位置,不重启仿真(区分点击和拖拽)
|
||||
// Chỉ ghi nhận vị trí, không restart simulation (phân biệt click và drag)
|
||||
d.fx = d.x
|
||||
d.fy = d.y
|
||||
d._dragStartX = event.x
|
||||
|
|
@ -669,13 +669,13 @@ const renderGraph = () => {
|
|||
d._isDragging = false
|
||||
})
|
||||
.on('drag', (event, d) => {
|
||||
// 检测是否真正开始拖拽(移动超过阈值)
|
||||
// Kiểm tra có thực sự bắt đầu drag hay chưa (dịch chuyển vượt ngưỡng)
|
||||
const dx = event.x - d._dragStartX
|
||||
const dy = event.y - d._dragStartY
|
||||
const distance = Math.sqrt(dx * dx + dy * dy)
|
||||
|
||||
if (!d._isDragging && distance > 3) {
|
||||
// 首次检测到真正拖拽,才重启仿真
|
||||
// Chỉ khi thật sự drag mới restart simulation
|
||||
d._isDragging = true
|
||||
simulation.alphaTarget(0.3).restart()
|
||||
}
|
||||
|
|
@ -686,7 +686,7 @@ const renderGraph = () => {
|
|||
}
|
||||
})
|
||||
.on('end', (event, d) => {
|
||||
// 只有真正拖拽过才让仿真逐渐停止
|
||||
// Chỉ khi thực sự drag mới cho simulation giảm dần
|
||||
if (d._isDragging) {
|
||||
simulation.alphaTarget(0)
|
||||
}
|
||||
|
|
@ -697,12 +697,12 @@ const renderGraph = () => {
|
|||
)
|
||||
.on('click', (event, d) => {
|
||||
event.stopPropagation()
|
||||
// 重置所有节点样式
|
||||
// Reset style của tất cả node
|
||||
node.attr('stroke', '#fff').attr('stroke-width', 2.5)
|
||||
linkGroup.selectAll('path').attr('stroke', '#C0C0C0').attr('stroke-width', 1.5)
|
||||
// 高亮选中节点
|
||||
// Highlight node được chọn
|
||||
d3.select(event.target).attr('stroke', '#E91E63').attr('stroke-width', 4)
|
||||
// 高亮与此节点相连的边
|
||||
// Highlight các cạnh nối với node này
|
||||
link.filter(l => l.source.id === d.id || l.target.id === d.id)
|
||||
.attr('stroke', '#E91E63')
|
||||
.attr('stroke-width', 2.5)
|
||||
|
|
@ -725,7 +725,7 @@ const renderGraph = () => {
|
|||
}
|
||||
})
|
||||
|
||||
// Node Labels
|
||||
// Nhãn node
|
||||
const nodeLabels = nodeGroup.selectAll('text')
|
||||
.data(nodes)
|
||||
.enter().append('text')
|
||||
|
|
@ -739,19 +739,19 @@ const renderGraph = () => {
|
|||
.style('font-family', 'system-ui, sans-serif')
|
||||
|
||||
simulation.on('tick', () => {
|
||||
// 更新曲线路径
|
||||
// Cập nhật đường cong
|
||||
link.attr('d', d => getLinkPath(d))
|
||||
|
||||
// 更新边标签位置(无旋转,水平显示更清晰)
|
||||
// Cập nhật vị trí nhãn cạnh (không xoay, hiển thị ngang rõ hơn)
|
||||
linkLabels.each(function(d) {
|
||||
const mid = getLinkMidpoint(d)
|
||||
d3.select(this)
|
||||
.attr('x', mid.x)
|
||||
.attr('y', mid.y)
|
||||
.attr('transform', '') // 移除旋转,保持水平
|
||||
.attr('transform', '') // Loại bỏ chuyển động xoay, giữ nguyên vị trí nằm ngang.
|
||||
})
|
||||
|
||||
// 更新边标签背景
|
||||
// Cập nhật nền nhãn cạnh
|
||||
linkLabelBg.each(function(d, i) {
|
||||
const mid = getLinkMidpoint(d)
|
||||
const textEl = linkLabels.nodes()[i]
|
||||
|
|
@ -761,7 +761,7 @@ const renderGraph = () => {
|
|||
.attr('y', mid.y - bbox.height / 2 - 2)
|
||||
.attr('width', bbox.width + 8)
|
||||
.attr('height', bbox.height + 4)
|
||||
.attr('transform', '') // 移除旋转
|
||||
.attr('transform', '') // Loại bỏ phép xoay
|
||||
})
|
||||
|
||||
node
|
||||
|
|
@ -773,7 +773,7 @@ const renderGraph = () => {
|
|||
.attr('y', d => d.y)
|
||||
})
|
||||
|
||||
// 点击空白处关闭详情面板
|
||||
// Click vào vùng trống để đóng panel chi tiết
|
||||
svg.on('click', () => {
|
||||
selectedItem.value = null
|
||||
node.attr('stroke', '#fff').attr('stroke-width', 2.5)
|
||||
|
|
@ -787,7 +787,7 @@ watch(() => props.graphData, () => {
|
|||
nextTick(renderGraph)
|
||||
}, { deep: true })
|
||||
|
||||
// 监听边标签显示开关
|
||||
// Theo dõi công tắc hiển thị nhãn cạnh
|
||||
watch(showEdgeLabels, (newVal) => {
|
||||
if (linkLabelsRef) {
|
||||
linkLabelsRef.style('display', newVal ? 'block' : 'none')
|
||||
|
|
@ -1250,7 +1250,7 @@ input:checked + .slider:before {
|
|||
50% { opacity: 1; transform: scale(1.15); filter: drop-shadow(0 0 8px rgba(76, 175, 80, 0.6)); }
|
||||
}
|
||||
|
||||
/* 模拟结束后的提示样式 */
|
||||
/* Style thông báo sau khi mô phỏng kết thúc */
|
||||
.graph-building-hint.finished-hint {
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
|
|
|||
|
|
@ -4,20 +4,20 @@
|
|||
:class="{ 'no-projects': projects.length === 0 && !loading }"
|
||||
ref="historyContainer"
|
||||
>
|
||||
<!-- 背景装饰:技术网格线(只在有项目时显示) -->
|
||||
<!-- Background decoration: tech grid lines (shown only when projects exist) -->
|
||||
<div v-if="projects.length > 0 || loading" class="tech-grid-bg">
|
||||
<div class="grid-pattern"></div>
|
||||
<div class="gradient-overlay"></div>
|
||||
</div>
|
||||
|
||||
<!-- 标题区域 -->
|
||||
<!-- Title section -->
|
||||
<div class="section-header">
|
||||
<div class="section-line"></div>
|
||||
<span class="section-title">推演记录</span>
|
||||
<span class="section-title">Simulation History</span>
|
||||
<div class="section-line"></div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片容器(只在有项目时显示) -->
|
||||
<!-- Card container (shown only when projects exist) -->
|
||||
<div v-if="projects.length > 0" class="cards-container" :class="{ expanded: isExpanded }" :style="containerStyle">
|
||||
<div
|
||||
v-for="(project, index) in projects"
|
||||
|
|
@ -29,33 +29,33 @@
|
|||
@mouseleave="hoveringCard = null"
|
||||
@click="navigateToProject(project)"
|
||||
>
|
||||
<!-- 卡片头部:simulation_id 和 功能可用状态 -->
|
||||
<!-- Card header: simulation_id and feature availability status -->
|
||||
<div class="card-header">
|
||||
<span class="card-id">{{ formatSimulationId(project.simulation_id) }}</span>
|
||||
<div class="card-status-icons">
|
||||
<span
|
||||
class="status-icon"
|
||||
:class="{ available: project.project_id, unavailable: !project.project_id }"
|
||||
title="图谱构建"
|
||||
title="Graph Build"
|
||||
>◇</span>
|
||||
<span
|
||||
class="status-icon available"
|
||||
title="环境搭建"
|
||||
title="Environment Setup"
|
||||
>◈</span>
|
||||
<span
|
||||
class="status-icon"
|
||||
:class="{ available: project.report_id, unavailable: !project.report_id }"
|
||||
title="分析报告"
|
||||
title="Analysis Report"
|
||||
>◆</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表区域 -->
|
||||
<!-- File list area -->
|
||||
<div class="card-files-wrapper">
|
||||
<!-- 角落装饰 - 取景框风格 -->
|
||||
<!-- Corner decoration - viewfinder style -->
|
||||
<div class="corner-mark top-left-only"></div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<!-- File list -->
|
||||
<div class="files-list" v-if="project.files && project.files.length > 0">
|
||||
<div
|
||||
v-for="(file, fileIndex) in project.files.slice(0, 3)"
|
||||
|
|
@ -65,25 +65,25 @@
|
|||
<span class="file-tag" :class="getFileType(file.filename)">{{ getFileTypeLabel(file.filename) }}</span>
|
||||
<span class="file-name">{{ truncateFilename(file.filename, 20) }}</span>
|
||||
</div>
|
||||
<!-- 如果有更多文件,显示提示 -->
|
||||
<!-- Show hint if there are more files -->
|
||||
<div v-if="project.files.length > 3" class="files-more">
|
||||
+{{ project.files.length - 3 }} 个文件
|
||||
+{{ project.files.length - 3 }} files
|
||||
</div>
|
||||
</div>
|
||||
<!-- 无文件时的占位 -->
|
||||
<!-- Placeholder when there are no files -->
|
||||
<div class="files-empty" v-else>
|
||||
<span class="empty-file-icon">◇</span>
|
||||
<span class="empty-file-text">暂无文件</span>
|
||||
<span class="empty-file-text">No files yet</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片标题(使用模拟需求的前20字作为标题) -->
|
||||
<!-- Card title (use first 20 characters of simulation requirement as title) -->
|
||||
<h3 class="card-title">{{ getSimulationTitle(project.simulation_requirement) }}</h3>
|
||||
|
||||
<!-- 卡片描述(模拟需求完整展示) -->
|
||||
<!-- Card description (full simulation requirement display) -->
|
||||
<p class="card-desc">{{ truncateText(project.simulation_requirement, 55) }}</p>
|
||||
|
||||
<!-- 卡片底部 -->
|
||||
<!-- Card footer -->
|
||||
<div class="card-footer">
|
||||
<div class="card-datetime">
|
||||
<span class="card-date">{{ formatDate(project.created_at) }}</span>
|
||||
|
|
@ -94,23 +94,23 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 底部装饰线 (hover时展开) -->
|
||||
<!-- Bottom decorative line (expands on hover) -->
|
||||
<div class="card-bottom-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<!-- Loading state -->
|
||||
<div v-if="loading" class="loading-state">
|
||||
<span class="loading-spinner"></span>
|
||||
<span class="loading-text">加载中...</span>
|
||||
<span class="loading-text">Loading...</span>
|
||||
</div>
|
||||
|
||||
<!-- 历史回放详情弹窗 -->
|
||||
<!-- History playback details modal -->
|
||||
<Teleport to="body">
|
||||
<Transition name="modal">
|
||||
<div v-if="selectedProject" class="modal-overlay" @click.self="closeModal">
|
||||
<div class="modal-content">
|
||||
<!-- 弹窗头部 -->
|
||||
<!-- Modal header -->
|
||||
<div class="modal-header">
|
||||
<div class="modal-title-section">
|
||||
<span class="modal-id">{{ formatSimulationId(selectedProject.simulation_id) }}</span>
|
||||
|
|
@ -122,35 +122,35 @@
|
|||
<button class="modal-close" @click="closeModal">×</button>
|
||||
</div>
|
||||
|
||||
<!-- 弹窗内容 -->
|
||||
<!-- Modal content -->
|
||||
<div class="modal-body">
|
||||
<!-- 模拟需求 -->
|
||||
<!-- Simulation requirement -->
|
||||
<div class="modal-section">
|
||||
<div class="modal-label">模拟需求</div>
|
||||
<div class="modal-requirement">{{ selectedProject.simulation_requirement || '无' }}</div>
|
||||
<div class="modal-label">Simulation Requirement</div>
|
||||
<div class="modal-requirement">{{ selectedProject.simulation_requirement || 'None' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 文件列表 -->
|
||||
<!-- File list -->
|
||||
<div class="modal-section">
|
||||
<div class="modal-label">关联文件</div>
|
||||
<div class="modal-label">Related Files</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>No related files</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 推演回放分割线 -->
|
||||
<!-- Simulation playback divider -->
|
||||
<div class="modal-divider">
|
||||
<span class="divider-line"></span>
|
||||
<span class="divider-text">推演回放</span>
|
||||
<span class="divider-text">Simulation Playback</span>
|
||||
<span class="divider-line"></span>
|
||||
</div>
|
||||
|
||||
<!-- 导航按钮 -->
|
||||
<!-- Navigation buttons -->
|
||||
<div class="modal-actions">
|
||||
<button
|
||||
class="modal-btn btn-project"
|
||||
|
|
@ -159,7 +159,7 @@
|
|||
>
|
||||
<span class="btn-step">Step1</span>
|
||||
<span class="btn-icon">◇</span>
|
||||
<span class="btn-text">图谱构建</span>
|
||||
<span class="btn-text">Graph Build</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">Environment Setup</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">Analysis Report</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- 不可回放提示 -->
|
||||
<!-- Playback unavailable hint -->
|
||||
<div class="modal-playback-hint">
|
||||
<span class="hint-text">Step3「开始模拟」与 Step5「深度互动」需在运行中启动,不支持历史回放</span>
|
||||
<span class="hint-text">Step3 "Start Simulation" and Step5 "Deep Interaction" must be launched during runtime and do not support historical playback</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -198,56 +198,56 @@ import { getSimulationHistory } from '../api/simulation'
|
|||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
// 状态
|
||||
// Trạng thái
|
||||
const projects = ref([])
|
||||
const loading = ref(true)
|
||||
const isExpanded = ref(false)
|
||||
const hoveringCard = ref(null)
|
||||
const historyContainer = ref(null)
|
||||
const selectedProject = ref(null) // 当前选中的项目(用于弹窗)
|
||||
const selectedProject = ref(null) // Dự án hiện được chọn (dùng cho modal)
|
||||
let observer = null
|
||||
let isAnimating = false // 动画锁,防止闪烁
|
||||
let expandDebounceTimer = null // 防抖定时器
|
||||
let pendingState = null // 记录待执行的目标状态
|
||||
let isAnimating = false // Khóa animation, tránh nhấp nháy
|
||||
let expandDebounceTimer = null // Bộ hẹn giờ debounce
|
||||
let pendingState = null // Ghi lại trạng thái đích chờ thực thi
|
||||
|
||||
// 卡片布局配置 - 调整为更宽的比例
|
||||
// Cấu hình layout thẻ - điều chỉnh sang tỷ lệ rộng hơn
|
||||
const CARDS_PER_ROW = 4
|
||||
const CARD_WIDTH = 280
|
||||
const CARD_HEIGHT = 280
|
||||
const CARD_GAP = 24
|
||||
|
||||
// 动态计算容器高度样式
|
||||
// Tính động style chiều cao container
|
||||
const containerStyle = computed(() => {
|
||||
if (!isExpanded.value) {
|
||||
// 折叠态:固定高度
|
||||
// Trạng thái thu gọn: chiều cao cố định
|
||||
return { minHeight: '420px' }
|
||||
}
|
||||
|
||||
// 展开态:根据卡片数量动态计算高度
|
||||
// Trạng thái mở rộng: tính chiều cao động theo số lượng thẻ
|
||||
const total = projects.value.length
|
||||
if (total === 0) {
|
||||
return { minHeight: '280px' }
|
||||
}
|
||||
|
||||
const rows = Math.ceil(total / CARDS_PER_ROW)
|
||||
// 计算实际需要的高度:行数 * 卡片高度 + (行数-1) * 间距 + 少量底部间距
|
||||
// Tính chiều cao thực tế cần dùng: số hàng * chiều cao thẻ + (số hàng-1) * khoảng cách + một ít khoảng đệm dưới
|
||||
const expandedHeight = rows * CARD_HEIGHT + (rows - 1) * CARD_GAP + 10
|
||||
|
||||
return { minHeight: `${expandedHeight}px` }
|
||||
})
|
||||
|
||||
// 获取卡片样式
|
||||
// Lấy style của thẻ
|
||||
const getCardStyle = (index) => {
|
||||
const total = projects.value.length
|
||||
|
||||
if (isExpanded.value) {
|
||||
// 展开态:网格布局
|
||||
// Trạng thái mở rộng: layout dạng lưới
|
||||
const transition = 'transform 700ms cubic-bezier(0.23, 1, 0.32, 1), opacity 700ms cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.3s ease, border-color 0.3s ease'
|
||||
|
||||
const col = index % CARDS_PER_ROW
|
||||
const row = Math.floor(index / CARDS_PER_ROW)
|
||||
|
||||
// 计算当前行的卡片数量,确保每行居中
|
||||
// Tính số lượng thẻ ở hàng hiện tại để căn giữa từng hàng
|
||||
const currentRowStart = row * CARDS_PER_ROW
|
||||
const currentRowCards = Math.min(CARDS_PER_ROW, total - currentRowStart)
|
||||
|
||||
|
|
@ -257,7 +257,7 @@ const getCardStyle = (index) => {
|
|||
const colInRow = index % CARDS_PER_ROW
|
||||
const x = startX + colInRow * (CARD_WIDTH + CARD_GAP)
|
||||
|
||||
// 向下展开,增加与标题的间距
|
||||
// Mở rộng xuống dưới, tăng khoảng cách với tiêu đề
|
||||
const y = 20 + row * (CARD_HEIGHT + CARD_GAP)
|
||||
|
||||
return {
|
||||
|
|
@ -267,14 +267,14 @@ const getCardStyle = (index) => {
|
|||
transition: transition
|
||||
}
|
||||
} else {
|
||||
// 折叠态:扇形堆叠
|
||||
// Trạng thái thu gọn: xếp chồng hình quạt
|
||||
const transition = 'transform 700ms cubic-bezier(0.23, 1, 0.32, 1), opacity 700ms cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.3s ease, border-color 0.3s ease'
|
||||
|
||||
const centerIndex = (total - 1) / 2
|
||||
const offset = index - centerIndex
|
||||
|
||||
const x = offset * 35
|
||||
// 调整起始位置,靠近标题但保持适当间距
|
||||
// Điều chỉnh vị trí bắt đầu, gần tiêu đề nhưng vẫn giữ khoảng cách phù hợp
|
||||
const y = 25 + Math.abs(offset) * 8
|
||||
const r = offset * 3
|
||||
const s = 0.95 - Math.abs(offset) * 0.05
|
||||
|
|
@ -288,24 +288,24 @@ const getCardStyle = (index) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 根据轮数进度获取样式类
|
||||
// Lấy class style theo tiến độ số vòng
|
||||
const getProgressClass = (simulation) => {
|
||||
const current = simulation.current_round || 0
|
||||
const total = simulation.total_rounds || 0
|
||||
|
||||
if (total === 0 || current === 0) {
|
||||
// 未开始
|
||||
// Chưa bắt đầu
|
||||
return 'not-started'
|
||||
} else if (current >= total) {
|
||||
// 已完成
|
||||
// Đã hoàn thành
|
||||
return 'completed'
|
||||
} else {
|
||||
// 进行中
|
||||
// Đang thực hiện
|
||||
return 'in-progress'
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期(只显示日期部分)
|
||||
// Format ngày tháng (chỉ hiển thị phần ngày)
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
|
|
@ -316,7 +316,7 @@ const formatDate = (dateStr) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 格式化时间(显示时:分)
|
||||
// Format thời gian (hiển thị giờ:phút)
|
||||
const formatTime = (dateStr) => {
|
||||
if (!dateStr) return ''
|
||||
try {
|
||||
|
|
@ -329,35 +329,35 @@ const formatTime = (dateStr) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 截断文本
|
||||
// Cắt ngắn văn bản
|
||||
const truncateText = (text, maxLength) => {
|
||||
if (!text) return ''
|
||||
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
|
||||
}
|
||||
|
||||
// 从模拟需求生成标题(取前20字)
|
||||
// Tạo tiêu đề từ yêu cầu mô phỏng (lấy 20 ký tự đầu)
|
||||
const getSimulationTitle = (requirement) => {
|
||||
if (!requirement) return '未命名模拟'
|
||||
if (!requirement) return 'Unnamed simulation'
|
||||
const title = requirement.slice(0, 20)
|
||||
return requirement.length > 20 ? title + '...' : title
|
||||
}
|
||||
|
||||
// 格式化 simulation_id 显示(截取前6位)
|
||||
// Format simulation_id để hiển thị (lấy 6 ký tự đầu)
|
||||
const formatSimulationId = (simulationId) => {
|
||||
if (!simulationId) return 'SIM_UNKNOWN'
|
||||
const prefix = simulationId.replace('sim_', '').slice(0, 6)
|
||||
return `SIM_${prefix.toUpperCase()}`
|
||||
}
|
||||
|
||||
// 格式化轮数显示(当前轮/总轮数)
|
||||
// Format hiển thị số vòng (vòng hiện tại/tổng số vòng)
|
||||
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 'Not started'
|
||||
return `${current}/${total} rounds`
|
||||
}
|
||||
|
||||
// 获取文件类型(用于样式)
|
||||
// Lấy loại file (dùng cho style)
|
||||
const getFileType = (filename) => {
|
||||
if (!filename) return 'other'
|
||||
const ext = filename.split('.').pop()?.toLowerCase()
|
||||
|
|
@ -373,16 +373,16 @@ const getFileType = (filename) => {
|
|||
return typeMap[ext] || 'other'
|
||||
}
|
||||
|
||||
// 获取文件类型标签文本
|
||||
// Lấy text nhãn loại file
|
||||
const getFileTypeLabel = (filename) => {
|
||||
if (!filename) return 'FILE'
|
||||
const ext = filename.split('.').pop()?.toUpperCase()
|
||||
return ext || 'FILE'
|
||||
}
|
||||
|
||||
// 截断文件名(保留扩展名)
|
||||
// Cắt ngắn tên file (giữ lại phần mở rộng)
|
||||
const truncateFilename = (filename, maxLength) => {
|
||||
if (!filename) return '未知文件'
|
||||
if (!filename) return 'Unknown file'
|
||||
if (filename.length <= maxLength) return filename
|
||||
|
||||
const ext = filename.includes('.') ? '.' + filename.split('.').pop() : ''
|
||||
|
|
@ -391,17 +391,17 @@ const truncateFilename = (filename, maxLength) => {
|
|||
return truncatedName + ext
|
||||
}
|
||||
|
||||
// 打开项目详情弹窗
|
||||
// Mở modal chi tiết dự án
|
||||
const navigateToProject = (simulation) => {
|
||||
selectedProject.value = simulation
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
// Đóng modal
|
||||
const closeModal = () => {
|
||||
selectedProject.value = null
|
||||
}
|
||||
|
||||
// 导航到图谱构建页面(Project)
|
||||
// Điều hướng đến trang xây dựng graph (Project)
|
||||
const goToProject = () => {
|
||||
if (selectedProject.value?.project_id) {
|
||||
router.push({
|
||||
|
|
@ -412,7 +412,7 @@ const goToProject = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 导航到环境配置页面(Simulation)
|
||||
// Điều hướng đến trang cấu hình môi trường (Simulation)
|
||||
const goToSimulation = () => {
|
||||
if (selectedProject.value?.simulation_id) {
|
||||
router.push({
|
||||
|
|
@ -423,7 +423,7 @@ const goToSimulation = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 导航到分析报告页面(Report)
|
||||
// Điều hướng đến trang báo cáo phân tích (Report)
|
||||
const goToReport = () => {
|
||||
if (selectedProject.value?.report_id) {
|
||||
router.push({
|
||||
|
|
@ -434,7 +434,7 @@ const goToReport = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 加载历史项目
|
||||
// Tải lịch sử dự án
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
|
@ -443,14 +443,14 @@ const loadHistory = async () => {
|
|||
projects.value = response.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史项目失败:', error)
|
||||
console.error('Failed to load history projects:', error)
|
||||
projects.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化 IntersectionObserver
|
||||
// Khởi tạo IntersectionObserver
|
||||
const initObserver = () => {
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
|
|
@ -461,47 +461,47 @@ const initObserver = () => {
|
|||
entries.forEach((entry) => {
|
||||
const shouldExpand = entry.isIntersecting
|
||||
|
||||
// 更新待执行的目标状态(无论是否在动画中都要记录最新的目标状态)
|
||||
// Cập nhật trạng thái đích chờ thực thi (dù đang animation vẫn phải ghi lại trạng thái mới nhất)
|
||||
pendingState = shouldExpand
|
||||
|
||||
// 清除之前的防抖定时器(新的滚动意图会覆盖旧的)
|
||||
// Xóa bộ hẹn giờ debounce trước đó (ý định cuộn mới sẽ ghi đè ý định cũ)
|
||||
if (expandDebounceTimer) {
|
||||
clearTimeout(expandDebounceTimer)
|
||||
expandDebounceTimer = null
|
||||
}
|
||||
|
||||
// 如果正在动画中,只记录状态,等动画结束后处理
|
||||
// Nếu đang animation thì chỉ ghi trạng thái, chờ xử lý sau khi animation kết thúc
|
||||
if (isAnimating) return
|
||||
|
||||
// 如果目标状态与当前状态相同,不需要处理
|
||||
// Nếu trạng thái đích trùng với trạng thái hiện tại thì không cần xử lý
|
||||
if (shouldExpand === isExpanded.value) {
|
||||
pendingState = null
|
||||
return
|
||||
}
|
||||
|
||||
// 使用防抖延迟状态切换,防止快速闪烁
|
||||
// 展开时延迟较短(50ms),收起时延迟较长(200ms)以增加稳定性
|
||||
// Dùng debounce để trì hoãn chuyển trạng thái, tránh nhấp nháy nhanh
|
||||
// Khi mở rộng thì delay ngắn hơn (50ms), khi thu gọn delay dài hơn (200ms) để ổn định hơn
|
||||
const delay = shouldExpand ? 50 : 200
|
||||
|
||||
expandDebounceTimer = setTimeout(() => {
|
||||
// 检查是否正在动画
|
||||
// Kiểm tra có đang animation không
|
||||
if (isAnimating) return
|
||||
|
||||
// 检查待执行状态是否仍需要执行(可能已被后续滚动覆盖)
|
||||
// Kiểm tra trạng thái chờ có còn cần thực thi không (có thể đã bị lần cuộn sau ghi đè)
|
||||
if (pendingState === null || pendingState === isExpanded.value) return
|
||||
|
||||
// 设置动画锁
|
||||
// Bật khóa animation
|
||||
isAnimating = true
|
||||
isExpanded.value = pendingState
|
||||
pendingState = null
|
||||
|
||||
// 动画完成后解除锁定,并检查是否有待处理的状态变化
|
||||
// Sau khi animation hoàn tất thì mở khóa, đồng thời kiểm tra xem có trạng thái mới đang chờ hay không
|
||||
setTimeout(() => {
|
||||
isAnimating = false
|
||||
|
||||
// 动画结束后,检查是否有新的待执行状态
|
||||
// Sau khi animation kết thúc, kiểm tra xem có trạng thái chờ mới không
|
||||
if (pendingState !== null && pendingState !== isExpanded.value) {
|
||||
// 延迟一小段时间再执行,避免太快切换
|
||||
// Delay thêm một chút trước khi thực thi để tránh chuyển quá nhanh
|
||||
expandDebounceTimer = setTimeout(() => {
|
||||
if (pendingState !== null && pendingState !== isExpanded.value) {
|
||||
isAnimating = true
|
||||
|
|
@ -518,20 +518,20 @@ const initObserver = () => {
|
|||
})
|
||||
},
|
||||
{
|
||||
// 使用多个阈值,使检测更平滑
|
||||
// Dùng nhiều ngưỡng để việc phát hiện mượt hơn
|
||||
threshold: [0.4, 0.6, 0.8],
|
||||
// 调整 rootMargin,视口底部向上收缩,需要滚动更多才触发展开
|
||||
// Điều chỉnh rootMargin, co đáy viewport lên trên để phải cuộn nhiều hơn mới kích hoạt mở rộng
|
||||
rootMargin: '0px 0px -150px 0px'
|
||||
}
|
||||
)
|
||||
|
||||
// 开始观察
|
||||
// Bắt đầu quan sát
|
||||
if (historyContainer.value) {
|
||||
observer.observe(historyContainer.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听路由变化,当返回首页时重新加载数据
|
||||
// Theo dõi thay đổi route, khi quay lại trang chủ thì tải lại dữ liệu
|
||||
watch(() => route.path, (newPath) => {
|
||||
if (newPath === '/') {
|
||||
loadHistory()
|
||||
|
|
@ -539,28 +539,28 @@ watch(() => route.path, (newPath) => {
|
|||
})
|
||||
|
||||
onMounted(async () => {
|
||||
// 确保 DOM 渲染完成后再加载数据
|
||||
// Đảm bảo DOM đã render xong rồi mới tải dữ liệu
|
||||
await nextTick()
|
||||
await loadHistory()
|
||||
|
||||
// 等待 DOM 渲染后初始化观察器
|
||||
// Chờ DOM render xong rồi mới khởi tạo observer
|
||||
setTimeout(() => {
|
||||
initObserver()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
// 如果使用 keep-alive,在组件激活时重新加载数据
|
||||
// Nếu dùng keep-alive, tải lại dữ liệu khi component được kích hoạt
|
||||
onActivated(() => {
|
||||
loadHistory()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理 Intersection Observer
|
||||
// Dọn dẹp Intersection Observer
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
observer = null
|
||||
}
|
||||
// 清理防抖定时器
|
||||
// Dọn dẹp bộ hẹn giờ debounce
|
||||
if (expandDebounceTimer) {
|
||||
clearTimeout(expandDebounceTimer)
|
||||
expandDebounceTimer = null
|
||||
|
|
@ -569,7 +569,7 @@ onUnmounted(() => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 容器 */
|
||||
/* Container */
|
||||
.history-database {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
|
@ -579,13 +579,13 @@ onUnmounted(() => {
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
/* 无项目时简化显示 */
|
||||
/* Simplified display when there are no projects */
|
||||
.history-database.no-projects {
|
||||
min-height: auto;
|
||||
padding: 40px 0 20px;
|
||||
}
|
||||
|
||||
/* 技术网格背景 */
|
||||
/* Tech grid background */
|
||||
.tech-grid-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -596,7 +596,7 @@ onUnmounted(() => {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 使用CSS背景图案创建固定间距的正方形网格 */
|
||||
/* Use CSS background pattern to create a square grid with fixed spacing */
|
||||
.grid-pattern {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
|
@ -607,7 +607,7 @@ onUnmounted(() => {
|
|||
linear-gradient(to right, rgba(0, 0, 0, 0.05) 1px, transparent 1px),
|
||||
linear-gradient(to bottom, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
|
||||
background-size: 50px 50px;
|
||||
/* 从左上角开始定位,高度变化时只在底部扩展,不影响已有网格位置 */
|
||||
/* Start positioning from top-left; when height changes it only expands at the bottom without affecting the existing grid position */
|
||||
background-position: top left;
|
||||
}
|
||||
|
||||
|
|
@ -623,7 +623,7 @@ onUnmounted(() => {
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 标题区域 */
|
||||
/* Title section */
|
||||
.section-header {
|
||||
position: relative;
|
||||
z-index: 100;
|
||||
|
|
@ -651,7 +651,7 @@ onUnmounted(() => {
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* 卡片容器 */
|
||||
/* Card container */
|
||||
.cards-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
@ -659,10 +659,10 @@ onUnmounted(() => {
|
|||
align-items: flex-start;
|
||||
padding: 0 40px;
|
||||
transition: min-height 700ms cubic-bezier(0.23, 1, 0.32, 1);
|
||||
/* min-height 由 JS 动态计算,根据卡片数量自适应 */
|
||||
/* min-height is dynamically calculated by JS and adapts to the number of cards */
|
||||
}
|
||||
|
||||
/* 项目卡片 */
|
||||
/* Project card */
|
||||
.project-card {
|
||||
position: absolute;
|
||||
width: 280px;
|
||||
|
|
@ -685,7 +685,7 @@ onUnmounted(() => {
|
|||
z-index: 1000 !important;
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
/* Card header */
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -703,7 +703,7 @@ onUnmounted(() => {
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 功能状态图标组 */
|
||||
/* Feature status icon group */
|
||||
.card-status-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -720,17 +720,17 @@ onUnmounted(() => {
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
/* 不同功能的颜色 */
|
||||
.status-icon:nth-child(1).available { color: #3B82F6; } /* 图谱构建 - 蓝色 */
|
||||
.status-icon:nth-child(2).available { color: #F59E0B; } /* 环境搭建 - 橙色 */
|
||||
.status-icon:nth-child(3).available { color: #10B981; } /* 分析报告 - 绿色 */
|
||||
/* Colors for different features */
|
||||
.status-icon:nth-child(1).available { color: #3B82F6; } /* Graph Build - blue */
|
||||
.status-icon:nth-child(2).available { color: #F59E0B; } /* Environment Setup - orange */
|
||||
.status-icon:nth-child(3).available { color: #10B981; } /* Analysis Report - green */
|
||||
|
||||
.status-icon.unavailable {
|
||||
color: #D1D5DB;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* 轮数进度显示 */
|
||||
/* Round progress display */
|
||||
.card-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -744,13 +744,13 @@ onUnmounted(() => {
|
|||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
/* 进度状态颜色 */
|
||||
.card-progress.completed { color: #10B981; } /* 已完成 - 绿色 */
|
||||
.card-progress.in-progress { color: #F59E0B; } /* 进行中 - 橙色 */
|
||||
.card-progress.not-started { color: #9CA3AF; } /* 未开始 - 灰色 */
|
||||
/* Progress status colors */
|
||||
.card-progress.completed { color: #10B981; } /* Completed - green */
|
||||
.card-progress.in-progress { color: #F59E0B; } /* In progress - orange */
|
||||
.card-progress.not-started { color: #9CA3AF; } /* Not started - gray */
|
||||
.card-status.pending { color: #9CA3AF; }
|
||||
|
||||
/* 文件列表区域 */
|
||||
/* File list area */
|
||||
.card-files-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
|
@ -770,7 +770,7 @@ onUnmounted(() => {
|
|||
gap: 4px;
|
||||
}
|
||||
|
||||
/* 更多文件提示 */
|
||||
/* More files hint */
|
||||
.files-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -800,7 +800,7 @@ onUnmounted(() => {
|
|||
border-color: #e5e7eb;
|
||||
}
|
||||
|
||||
/* 简约文件标签样式 */
|
||||
/* Minimal file tag style */
|
||||
.file-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
@ -818,7 +818,7 @@ onUnmounted(() => {
|
|||
min-width: 28px;
|
||||
}
|
||||
|
||||
/* 低饱和度配色方案 - Morandi色系 */
|
||||
/* Low-saturation palette - Morandi style */
|
||||
.file-tag.pdf { background: #f2e6e6; color: #a65a5a; }
|
||||
.file-tag.doc { background: #e6eff5; color: #5a7ea6; }
|
||||
.file-tag.xls { background: #e6f2e8; color: #5aa668; }
|
||||
|
|
@ -839,7 +839,7 @@ onUnmounted(() => {
|
|||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
/* 无文件时的占位 */
|
||||
/* Placeholder when there are no files */
|
||||
.files-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -860,13 +860,13 @@ onUnmounted(() => {
|
|||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* 悬停时文件区域效果 */
|
||||
/* File area effect on hover */
|
||||
.project-card:hover .card-files-wrapper {
|
||||
border-color: #d1d5db;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
}
|
||||
|
||||
/* 角落装饰 */
|
||||
/* Corner decoration */
|
||||
.corner-mark.top-left-only {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
|
|
@ -879,7 +879,7 @@ onUnmounted(() => {
|
|||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 卡片标题 */
|
||||
/* Card title */
|
||||
.card-title {
|
||||
font-family: 'Inter', -apple-system, sans-serif;
|
||||
font-size: 0.9rem;
|
||||
|
|
@ -897,7 +897,7 @@ onUnmounted(() => {
|
|||
color: #2563EB;
|
||||
}
|
||||
|
||||
/* 卡片描述 */
|
||||
/* Card description */
|
||||
.card-desc {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.75rem;
|
||||
|
|
@ -911,7 +911,7 @@ onUnmounted(() => {
|
|||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* 卡片底部 */
|
||||
/* Card footer */
|
||||
.card-footer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
@ -925,14 +925,14 @@ onUnmounted(() => {
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 日期时间组合 */
|
||||
/* Date and time group */
|
||||
.card-datetime {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 底部轮数进度显示 */
|
||||
/* Bottom round progress display */
|
||||
.card-footer .card-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -946,12 +946,12 @@ onUnmounted(() => {
|
|||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
/* 进度状态颜色 - 底部 */
|
||||
/* Progress status colors - bottom */
|
||||
.card-footer .card-progress.completed { color: #10B981; }
|
||||
.card-footer .card-progress.in-progress { color: #F59E0B; }
|
||||
.card-footer .card-progress.not-started { color: #9CA3AF; }
|
||||
|
||||
/* 底部装饰线 */
|
||||
/* Bottom decorative line */
|
||||
.card-bottom-line {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
|
|
@ -967,7 +967,7 @@ onUnmounted(() => {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
/* Empty state */
|
||||
.empty-state, .loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -995,7 +995,7 @@ onUnmounted(() => {
|
|||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式 */
|
||||
/* Responsive */
|
||||
@media (max-width: 1200px) {
|
||||
.project-card {
|
||||
width: 240px;
|
||||
|
|
@ -1011,7 +1011,7 @@ onUnmounted(() => {
|
|||
}
|
||||
}
|
||||
|
||||
/* ===== 历史回放详情弹窗样式 ===== */
|
||||
/* ===== Style modal chi tiết phát lại lịch sử ===== */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
|
|
@ -1037,7 +1037,7 @@ onUnmounted(() => {
|
|||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
/* 动画过渡 */
|
||||
/* Hiệu ứng chuyển động */
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
|
|
@ -1066,7 +1066,7 @@ onUnmounted(() => {
|
|||
opacity: 0;
|
||||
}
|
||||
|
||||
/* 弹窗头部 */
|
||||
/* Phần đầu modal */
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -1133,7 +1133,7 @@ onUnmounted(() => {
|
|||
color: #111827;
|
||||
}
|
||||
|
||||
/* 弹窗内容 */
|
||||
/* Nội dung modal */
|
||||
.modal-body {
|
||||
padding: 24px 32px;
|
||||
}
|
||||
|
|
@ -1175,7 +1175,7 @@ onUnmounted(() => {
|
|||
padding-right: 4px;
|
||||
}
|
||||
|
||||
/* 自定义滚动条样式 */
|
||||
/* Style thanh cuộn tùy chỉnh */
|
||||
.modal-files::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
|
@ -1229,7 +1229,7 @@ onUnmounted(() => {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
/* 推演回放分割线 */
|
||||
/* Đường phân cách phát lại mô phỏng */
|
||||
.modal-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -1253,7 +1253,7 @@ onUnmounted(() => {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 导航按钮 */
|
||||
/* Nút điều hướng */
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
|
@ -1320,7 +1320,7 @@ onUnmounted(() => {
|
|||
color: #111827;
|
||||
}
|
||||
|
||||
/* 不可回放提示 */
|
||||
/* Gợi ý không thể phát lại */
|
||||
.modal-playback-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1,30 +1,30 @@
|
|||
<template>
|
||||
<div class="workbench-panel">
|
||||
<div class="scroll-container">
|
||||
<!-- Step 01: Ontology -->
|
||||
<!-- Bước 01: Ontology -->
|
||||
<div class="step-card" :class="{ 'active': currentPhase === 0, 'completed': currentPhase > 0 }">
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">01</span>
|
||||
<span class="step-title">本体生成</span>
|
||||
<span class="step-title">Ontology Generation</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">Completed</span>
|
||||
<span v-else-if="currentPhase === 0" class="badge processing">Processing</span>
|
||||
<span v-else class="badge pending">Pending</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/graph/ontology/generate</p>
|
||||
<p class="description">
|
||||
LLM分析文档内容与模拟需求,提取出现实种子,自动生成合适的本体结构
|
||||
LLM analyzes document content and simulation requirements, extracts real-world seeds, and automatically generates a suitable ontology structure
|
||||
</p>
|
||||
|
||||
<!-- Loading / Progress -->
|
||||
<div v-if="currentPhase === 0 && ontologyProgress" class="progress-section">
|
||||
<div class="spinner-sm"></div>
|
||||
<span>{{ ontologyProgress.message || '正在分析文档...' }}</span>
|
||||
<span>{{ ontologyProgress.message || 'Analyzing document...' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Detail Overlay -->
|
||||
|
|
@ -105,72 +105,72 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 02: Graph Build -->
|
||||
<!-- Bước 02: Xây dựng Graph -->
|
||||
<div class="step-card" :class="{ 'active': currentPhase === 1, 'completed': currentPhase > 1 }">
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">02</span>
|
||||
<span class="step-title">GraphRAG构建</span>
|
||||
<span class="step-title">GraphRAG Build</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="currentPhase > 1" class="badge success">已完成</span>
|
||||
<span v-if="currentPhase > 1" class="badge success">Completed</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">Pending</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/graph/build</p>
|
||||
<p class="description">
|
||||
基于生成的本体,将文档自动分块后调用 Zep 构建知识图谱,提取实体和关系,并形成时序记忆与社区摘要
|
||||
Based on the generated ontology, documents are automatically chunked and Zep is used to build the knowledge graph, extract entities and relationships, and generate temporal memory and community summaries
|
||||
</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">Entity Nodes</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ graphStats.edges }}</span>
|
||||
<span class="stat-label">关系边</span>
|
||||
<span class="stat-label">Relation Edges</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{ graphStats.types }}</span>
|
||||
<span class="stat-label">SCHEMA类型</span>
|
||||
<span class="stat-label">Schema Types</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 03: Complete -->
|
||||
<!-- Bước 03: Hoàn thành -->
|
||||
<div class="step-card" :class="{ 'active': currentPhase === 2, 'completed': currentPhase >= 2 }">
|
||||
<div class="card-header">
|
||||
<div class="step-info">
|
||||
<span class="step-num">03</span>
|
||||
<span class="step-title">构建完成</span>
|
||||
<span class="step-title">Build Complete</span>
|
||||
</div>
|
||||
<div class="step-status">
|
||||
<span v-if="currentPhase >= 2" class="badge accent">进行中</span>
|
||||
<span v-if="currentPhase >= 2" class="badge accent">In Progress</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-content">
|
||||
<p class="api-note">POST /api/simulation/create</p>
|
||||
<p class="description">图谱构建已完成,请进入下一步进行模拟环境搭建</p>
|
||||
<p class="description">Graph build completed, proceed to next step to set up simulation environment</p>
|
||||
<button
|
||||
class="action-btn"
|
||||
:disabled="currentPhase < 2 || creatingSimulation"
|
||||
@click="handleEnterEnvSetup"
|
||||
>
|
||||
<span v-if="creatingSimulation" class="spinner-sm"></span>
|
||||
{{ creatingSimulation ? '创建中...' : '进入环境搭建 ➝' }}
|
||||
{{ creatingSimulation ? 'Creating...' : 'Enter Environment Setup ➝' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Info / Logs -->
|
||||
<!-- Log hệ thống -->
|
||||
<div class="system-logs">
|
||||
<div class="log-header">
|
||||
<span class="log-title">SYSTEM DASHBOARD</span>
|
||||
|
|
@ -208,10 +208,10 @@ const selectedOntologyItem = ref(null)
|
|||
const logContent = ref(null)
|
||||
const creatingSimulation = ref(false)
|
||||
|
||||
// 进入环境搭建 - 创建 simulation 并跳转
|
||||
// Vào bước setup môi trường - tạo simulation và chuyển trang
|
||||
const handleEnterEnvSetup = async () => {
|
||||
if (!props.projectData?.project_id || !props.projectData?.graph_id) {
|
||||
console.error('缺少项目或图谱信息')
|
||||
console.error('Missing project or graph info')
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -226,18 +226,18 @@ const handleEnterEnvSetup = async () => {
|
|||
})
|
||||
|
||||
if (res.success && res.data?.simulation_id) {
|
||||
// 跳转到 simulation 页面
|
||||
// Chuyển đến trang mô phỏng
|
||||
router.push({
|
||||
name: 'Simulation',
|
||||
params: { simulationId: res.data.simulation_id }
|
||||
})
|
||||
} else {
|
||||
console.error('创建模拟失败:', res.error)
|
||||
alert('创建模拟失败: ' + (res.error || '未知错误'))
|
||||
console.error('Create simulation failed:', res.error)
|
||||
alert('Create simulation failed: ' + (res.error || 'Unknown error'))
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('创建模拟异常:', err)
|
||||
alert('创建模拟异常: ' + err.message)
|
||||
console.error('Create simulation exception:', err)
|
||||
alert('Create simulation exception: ' + err.message)
|
||||
} finally {
|
||||
creatingSimulation.value = false
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,7 +3,7 @@
|
|||
<!-- Top Control Bar -->
|
||||
<div class="control-bar">
|
||||
<div class="status-group">
|
||||
<!-- Twitter 平台进度 -->
|
||||
<!-- Twitter platform progress -->
|
||||
<div class="platform-status twitter" :class="{ active: runStatus.twitter_running, completed: runStatus.twitter_completed }">
|
||||
<div class="platform-header">
|
||||
<svg class="platform-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
<span class="stat-value mono">{{ runStatus.twitter_actions_count || 0 }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 可用动作提示 -->
|
||||
<!-- Gợi ý các hành động khả dụng -->
|
||||
<div class="actions-tooltip">
|
||||
<div class="tooltip-title">Available Actions</div>
|
||||
<div class="tooltip-actions">
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Reddit 平台进度 -->
|
||||
<!-- Reddit platform progress -->
|
||||
<div class="platform-status reddit" :class="{ active: runStatus.reddit_running, completed: runStatus.reddit_completed }">
|
||||
<div class="platform-header">
|
||||
<svg class="platform-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2">
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<span class="stat-value mono">{{ runStatus.reddit_actions_count || 0 }}</span>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 可用动作提示 -->
|
||||
<!-- Gợi ý các hành động khả dụng -->
|
||||
<div class="actions-tooltip">
|
||||
<div class="tooltip-title">Available Actions</div>
|
||||
<div class="tooltip-actions">
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
@click="handleNextStep"
|
||||
>
|
||||
<span v-if="isGeneratingReport" class="loading-spinner-small"></span>
|
||||
{{ isGeneratingReport ? '启动中...' : '开始生成结果报告' }}
|
||||
{{ isGeneratingReport ? 'Starting...' : 'Start Generating Report' }}
|
||||
<span v-if="!isGeneratingReport" class="arrow-icon">→</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -157,12 +157,12 @@
|
|||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<!-- CREATE_POST: 发布帖子 -->
|
||||
<!-- CREATE_POST: Publish post -->
|
||||
<div v-if="action.action_type === 'CREATE_POST' && action.action_args?.content" class="content-text main-text">
|
||||
{{ action.action_args.content }}
|
||||
</div>
|
||||
|
||||
<!-- QUOTE_POST: 引用帖子 -->
|
||||
<!-- QUOTE_POST: Quote post -->
|
||||
<template v-if="action.action_type === 'QUOTE_POST'">
|
||||
<div v-if="action.action_args?.quote_content" class="content-text">
|
||||
{{ action.action_args.quote_content }}
|
||||
|
|
@ -178,7 +178,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- REPOST: 转发帖子 -->
|
||||
<!-- REPOST: Repost post -->
|
||||
<template v-if="action.action_type === 'REPOST'">
|
||||
<div class="repost-info">
|
||||
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>
|
||||
|
|
@ -189,7 +189,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- LIKE_POST: 点赞帖子 -->
|
||||
<!-- LIKE_POST: Like post -->
|
||||
<template v-if="action.action_type === 'LIKE_POST'">
|
||||
<div class="like-info">
|
||||
<svg class="icon-small filled" viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
||||
|
|
@ -200,7 +200,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- CREATE_COMMENT: 发表评论 -->
|
||||
<!-- CREATE_COMMENT: Publish comment -->
|
||||
<template v-if="action.action_type === 'CREATE_COMMENT'">
|
||||
<div v-if="action.action_args?.content" class="content-text">
|
||||
{{ action.action_args.content }}
|
||||
|
|
@ -211,7 +211,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- SEARCH_POSTS: 搜索帖子 -->
|
||||
<!-- SEARCH_POSTS: Search posts -->
|
||||
<template v-if="action.action_type === 'SEARCH_POSTS'">
|
||||
<div class="search-info">
|
||||
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></svg>
|
||||
|
|
@ -220,7 +220,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- FOLLOW: 关注用户 -->
|
||||
<!-- FOLLOW: Follow user -->
|
||||
<template v-if="action.action_type === 'FOLLOW'">
|
||||
<div class="follow-info">
|
||||
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="8.5" cy="7" r="4"></circle><line x1="20" y1="8" x2="20" y2="14"></line><line x1="23" y1="11" x2="17" y2="11"></line></svg>
|
||||
|
|
@ -240,7 +240,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- DO_NOTHING: 无操作(静默) -->
|
||||
<!-- DO_NOTHING: Không thao tác (im lặng) -->
|
||||
<template v-if="action.action_type === 'DO_NOTHING'">
|
||||
<div class="idle-info">
|
||||
<svg class="icon-small" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
|
||||
|
|
@ -248,7 +248,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 通用回退:未知类型或有 content 但未被上述处理 -->
|
||||
<!-- Fallback chung: loại chưa biết hoặc có content nhưng chưa được xử lý ở trên -->
|
||||
<div v-if="!['CREATE_POST', 'QUOTE_POST', 'REPOST', 'LIKE_POST', 'CREATE_COMMENT', 'SEARCH_POSTS', 'FOLLOW', 'UPVOTE_POST', 'DOWNVOTE_POST', 'DO_NOTHING'].includes(action.action_type) && action.action_args?.content" class="content-text">
|
||||
{{ action.action_args.content }}
|
||||
</div>
|
||||
|
|
@ -256,7 +256,7 @@
|
|||
|
||||
<div class="card-footer">
|
||||
<span class="time-tag">R{{ action.round_num }} • {{ formatActionTime(action.timestamp) }}</span>
|
||||
<!-- Platform tag removed as it is in header now -->
|
||||
<!-- Đã bỏ platform tag vì đã có ở header -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -298,10 +298,10 @@ import { generateReport } from '../api/report'
|
|||
|
||||
const props = defineProps({
|
||||
simulationId: String,
|
||||
maxRounds: Number, // 从Step2传入的最大轮数
|
||||
maxRounds: Number, // max rounds được truyền từ Step2
|
||||
minutesPerRound: {
|
||||
type: Number,
|
||||
default: 30 // 默认每轮30分钟
|
||||
default: 30 // mặc định mỗi round là 30 phút
|
||||
},
|
||||
projectData: Object,
|
||||
graphData: Object,
|
||||
|
|
@ -314,22 +314,22 @@ const router = useRouter()
|
|||
|
||||
// State
|
||||
const isGeneratingReport = ref(false)
|
||||
const phase = ref(0) // 0: 未开始, 1: 运行中, 2: 已完成
|
||||
const phase = ref(0) // 0: chưa bắt đầu, 1: đang chạy, 2: đã hoàn thành
|
||||
const isStarting = ref(false)
|
||||
const isStopping = ref(false)
|
||||
const startError = ref(null)
|
||||
const runStatus = ref({})
|
||||
const allActions = ref([]) // 所有动作(增量累积)
|
||||
const actionIds = ref(new Set()) // 用于去重的动作ID集合
|
||||
const allActions = ref([]) // toàn bộ action (cộng dồn tăng dần)
|
||||
const actionIds = ref(new Set()) // tập ID action để khử trùng lặp
|
||||
const scrollContainer = ref(null)
|
||||
|
||||
// Computed
|
||||
// 按时间顺序显示动作(最新的在最后面,即底部)
|
||||
// Hiển thị action theo thứ tự thời gian (mới nhất ở cuối, tức phía dưới)
|
||||
const chronologicalActions = computed(() => {
|
||||
return allActions.value
|
||||
})
|
||||
|
||||
// 各平台动作计数
|
||||
// Đếm action của từng platform
|
||||
const twitterActionsCount = computed(() => {
|
||||
return allActions.value.filter(a => a.platform === 'twitter').length
|
||||
})
|
||||
|
|
@ -338,7 +338,7 @@ const redditActionsCount = computed(() => {
|
|||
return allActions.value.filter(a => a.platform === 'reddit').length
|
||||
})
|
||||
|
||||
// 格式化模拟流逝时间(根据轮次和每轮分钟数计算)
|
||||
// Format thời gian mô phỏng đã trôi qua (tính theo round và phút mỗi round)
|
||||
const formatElapsedTime = (currentRound) => {
|
||||
if (!currentRound || currentRound <= 0) return '0h 0m'
|
||||
const totalMinutes = currentRound * props.minutesPerRound
|
||||
|
|
@ -347,12 +347,12 @@ const formatElapsedTime = (currentRound) => {
|
|||
return `${hours}h ${minutes}m`
|
||||
}
|
||||
|
||||
// Twitter平台的模拟流逝时间
|
||||
// Thời gian mô phỏng đã trôi qua của Twitter
|
||||
const twitterElapsedTime = computed(() => {
|
||||
return formatElapsedTime(runStatus.value.twitter_current_round || 0)
|
||||
})
|
||||
|
||||
// Reddit平台的模拟流逝时间
|
||||
// Thời gian mô phỏng đã trôi qua của Reddit
|
||||
const redditElapsedTime = computed(() => {
|
||||
return formatElapsedTime(runStatus.value.reddit_current_round || 0)
|
||||
})
|
||||
|
|
@ -362,7 +362,7 @@ const addLog = (msg) => {
|
|||
emit('add-log', msg)
|
||||
}
|
||||
|
||||
// 重置所有状态(用于重新启动模拟)
|
||||
// Reset toàn bộ trạng thái (dùng để khởi động lại mô phỏng)
|
||||
const resetAllState = () => {
|
||||
phase.value = 0
|
||||
runStatus.value = {}
|
||||
|
|
@ -373,46 +373,46 @@ const resetAllState = () => {
|
|||
startError.value = null
|
||||
isStarting.value = false
|
||||
isStopping.value = false
|
||||
stopPolling() // 停止之前可能存在的轮询
|
||||
stopPolling() // dừng các polling cũ nếu có
|
||||
}
|
||||
|
||||
// 启动模拟
|
||||
// Khởi động mô phỏng
|
||||
const doStartSimulation = async () => {
|
||||
if (!props.simulationId) {
|
||||
addLog('错误:缺少 simulationId')
|
||||
addLog('Error: missing simulationId')
|
||||
return
|
||||
}
|
||||
|
||||
// 先重置所有状态,确保不会受到上一次模拟的影响
|
||||
// Reset toàn bộ state trước để tránh bị ảnh hưởng từ lần mô phỏng trước
|
||||
resetAllState()
|
||||
|
||||
isStarting.value = true
|
||||
startError.value = null
|
||||
addLog('正在启动双平台并行模拟...')
|
||||
addLog('Starting dual-platform parallel simulation...')
|
||||
emit('update-status', 'processing')
|
||||
|
||||
try {
|
||||
const params = {
|
||||
simulation_id: props.simulationId,
|
||||
platform: 'parallel',
|
||||
force: true, // 强制重新开始
|
||||
enable_graph_memory_update: true // 开启动态图谱更新
|
||||
force: true, // ép bắt đầu lại từ đầu
|
||||
enable_graph_memory_update: true // bật cập nhật graph động
|
||||
}
|
||||
|
||||
if (props.maxRounds) {
|
||||
params.max_rounds = props.maxRounds
|
||||
addLog(`设置最大模拟轮数: ${props.maxRounds}`)
|
||||
addLog(`Set maximum simulation rounds: ${props.maxRounds}`)
|
||||
}
|
||||
|
||||
addLog('已开启动态图谱更新模式')
|
||||
addLog('Dynamic graph memory update mode enabled')
|
||||
|
||||
const res = await startSimulation(params)
|
||||
|
||||
if (res.success && res.data) {
|
||||
if (res.data.force_restarted) {
|
||||
addLog('✓ 已清理旧的模拟日志,重新开始模拟')
|
||||
addLog('✓ Old simulation logs cleared, restarting simulation')
|
||||
}
|
||||
addLog('✓ 模拟引擎启动成功')
|
||||
addLog('✓ Simulation engine started successfully')
|
||||
addLog(` ├─ PID: ${res.data.process_pid || '-'}`)
|
||||
|
||||
phase.value = 1
|
||||
|
|
@ -421,45 +421,45 @@ const doStartSimulation = async () => {
|
|||
startStatusPolling()
|
||||
startDetailPolling()
|
||||
} else {
|
||||
startError.value = res.error || '启动失败'
|
||||
addLog(`✗ 启动失败: ${res.error || '未知错误'}`)
|
||||
startError.value = res.error || 'Start failed'
|
||||
addLog(`✗ Start failed: ${res.error || 'Unknown error'}`)
|
||||
emit('update-status', 'error')
|
||||
}
|
||||
} catch (err) {
|
||||
startError.value = err.message
|
||||
addLog(`✗ 启动异常: ${err.message}`)
|
||||
addLog(`✗ Start exception: ${err.message}`)
|
||||
emit('update-status', 'error')
|
||||
} finally {
|
||||
isStarting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 停止模拟
|
||||
// Dừng mô phỏng
|
||||
const handleStopSimulation = async () => {
|
||||
if (!props.simulationId) return
|
||||
|
||||
isStopping.value = true
|
||||
addLog('正在停止模拟...')
|
||||
addLog('Stopping simulation...')
|
||||
|
||||
try {
|
||||
const res = await stopSimulation({ simulation_id: props.simulationId })
|
||||
|
||||
if (res.success) {
|
||||
addLog('✓ 模拟已停止')
|
||||
addLog('✓ Simulation stopped')
|
||||
phase.value = 2
|
||||
stopPolling()
|
||||
emit('update-status', 'completed')
|
||||
} else {
|
||||
addLog(`停止失败: ${res.error || '未知错误'}`)
|
||||
addLog(`Stop failed: ${res.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`停止异常: ${err.message}`)
|
||||
addLog(`Stop exception: ${err.message}`)
|
||||
} finally {
|
||||
isStopping.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 轮询状态
|
||||
// Polling trạng thái
|
||||
let statusTimer = null
|
||||
let detailTimer = null
|
||||
|
||||
|
|
@ -482,7 +482,7 @@ const stopPolling = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 追踪各平台的上一次轮次,用于检测变化并输出日志
|
||||
// Theo dõi round trước đó của từng platform để phát hiện thay đổi và ghi log
|
||||
const prevTwitterRound = ref(0)
|
||||
const prevRedditRound = ref(0)
|
||||
|
||||
|
|
@ -497,7 +497,7 @@ const fetchRunStatus = async () => {
|
|||
|
||||
runStatus.value = data
|
||||
|
||||
// 分别检测各平台的轮次变化并输出日志
|
||||
// Kiểm tra thay đổi round của từng platform và ghi log
|
||||
if (data.twitter_current_round > prevTwitterRound.value) {
|
||||
addLog(`[Plaza] R${data.twitter_current_round}/${data.total_rounds} | T:${data.twitter_simulated_hours || 0}h | A:${data.twitter_actions_count}`)
|
||||
prevTwitterRound.value = data.twitter_current_round
|
||||
|
|
@ -508,46 +508,46 @@ const fetchRunStatus = async () => {
|
|||
prevRedditRound.value = data.reddit_current_round
|
||||
}
|
||||
|
||||
// 检测模拟是否已完成(通过 runner_status 或平台完成状态判断)
|
||||
// Kiểm tra mô phỏng đã hoàn thành chưa (theo runner_status hoặc trạng thái hoàn thành của platform)
|
||||
const isCompleted = data.runner_status === 'completed' || data.runner_status === 'stopped'
|
||||
|
||||
// 额外检查:如果后端还没来得及更新 runner_status,但平台已经报告完成
|
||||
// 通过检测 twitter_completed 和 reddit_completed 状态判断
|
||||
// Kiểm tra thêm: nếu backend chưa kịp cập nhật runner_status nhưng platform đã báo xong
|
||||
// Dựa vào twitter_completed và reddit_completed để xác định
|
||||
const platformsCompleted = checkPlatformsCompleted(data)
|
||||
|
||||
if (isCompleted || platformsCompleted) {
|
||||
if (platformsCompleted && !isCompleted) {
|
||||
addLog('✓ 检测到所有平台模拟已结束')
|
||||
addLog('✓ Detected all platform simulations have finished')
|
||||
}
|
||||
addLog('✓ 模拟已完成')
|
||||
addLog('✓ Simulation completed')
|
||||
phase.value = 2
|
||||
stopPolling()
|
||||
emit('update-status', 'completed')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('获取运行状态失败:', err)
|
||||
console.warn('Failed to fetch run status:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查所有启用的平台是否已完成
|
||||
// Kiểm tra tất cả platform đang bật đã hoàn thành chưa
|
||||
const checkPlatformsCompleted = (data) => {
|
||||
// 如果没有任何平台数据,返回 false
|
||||
// Nếu không có dữ liệu platform nào thì trả về false
|
||||
if (!data) return false
|
||||
|
||||
// 检查各平台的完成状态
|
||||
// Kiểm tra trạng thái hoàn thành của từng platform
|
||||
const twitterCompleted = data.twitter_completed === true
|
||||
const redditCompleted = data.reddit_completed === true
|
||||
|
||||
// 如果至少有一个平台完成了,检查是否所有启用的平台都完成了
|
||||
// 通过 actions_count 判断平台是否被启用(如果 count > 0 或 running 曾为 true)
|
||||
// Nếu có ít nhất một platform hoàn thành, kiểm tra xem tất cả platform đang bật đã hoàn thành chưa
|
||||
// Dùng actions_count để suy ra platform có được bật không (count > 0 hoặc từng running)
|
||||
const twitterEnabled = (data.twitter_actions_count > 0) || data.twitter_running || twitterCompleted
|
||||
const redditEnabled = (data.reddit_actions_count > 0) || data.reddit_running || redditCompleted
|
||||
|
||||
// 如果没有任何平台被启用,返回 false
|
||||
// Nếu không có platform nào được bật thì trả về false
|
||||
if (!twitterEnabled && !redditEnabled) return false
|
||||
|
||||
// 检查所有启用的平台是否都已完成
|
||||
// Kiểm tra tất cả platform đang bật đã hoàn thành chưa
|
||||
if (twitterEnabled && !twitterCompleted) return false
|
||||
if (redditEnabled && !redditCompleted) return false
|
||||
|
||||
|
|
@ -561,13 +561,13 @@ const fetchRunStatusDetail = async () => {
|
|||
const res = await getRunStatusDetail(props.simulationId)
|
||||
|
||||
if (res.success && res.data) {
|
||||
// 使用 all_actions 获取完整的动作列表
|
||||
// Dùng all_actions để lấy đầy đủ danh sách action
|
||||
const serverActions = res.data.all_actions || []
|
||||
|
||||
// 增量添加新动作(去重)
|
||||
// Thêm tăng dần các action mới (khử trùng lặp)
|
||||
let newActionsAdded = 0
|
||||
serverActions.forEach(action => {
|
||||
// 生成唯一ID
|
||||
// Tạo unique ID
|
||||
const actionId = action.id || `${action.timestamp}-${action.platform}-${action.agent_id}-${action.action_type}`
|
||||
|
||||
if (!actionIds.value.has(actionId)) {
|
||||
|
|
@ -580,11 +580,11 @@ const fetchRunStatusDetail = async () => {
|
|||
}
|
||||
})
|
||||
|
||||
// 不自动滚动,让用户自由查看时间轴
|
||||
// 新动作会在底部追加
|
||||
// Không tự động cuộn, để người dùng tự do xem timeline
|
||||
// Action mới sẽ được thêm ở phía dưới
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('获取详细状态失败:', err)
|
||||
console.warn('Failed to fetch detailed status:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -640,17 +640,17 @@ const formatActionTime = (timestamp) => {
|
|||
|
||||
const handleNextStep = async () => {
|
||||
if (!props.simulationId) {
|
||||
addLog('错误:缺少 simulationId')
|
||||
addLog('Error: missing simulationId')
|
||||
return
|
||||
}
|
||||
|
||||
if (isGeneratingReport.value) {
|
||||
addLog('报告生成请求已发送,请稍候...')
|
||||
addLog('Report generation request has been sent, please wait...')
|
||||
return
|
||||
}
|
||||
|
||||
isGeneratingReport.value = true
|
||||
addLog('正在启动报告生成...')
|
||||
addLog('Starting report generation...')
|
||||
|
||||
try {
|
||||
const res = await generateReport({
|
||||
|
|
@ -660,21 +660,21 @@ const handleNextStep = async () => {
|
|||
|
||||
if (res.success && res.data) {
|
||||
const reportId = res.data.report_id
|
||||
addLog(`✓ 报告生成任务已启动: ${reportId}`)
|
||||
addLog(`✓ Report generation task started: ${reportId}`)
|
||||
|
||||
// 跳转到报告页面
|
||||
// Chuyển sang trang report
|
||||
router.push({ name: 'Report', params: { reportId } })
|
||||
} else {
|
||||
addLog(`✗ 启动报告生成失败: ${res.error || '未知错误'}`)
|
||||
addLog(`✗ Failed to start report generation: ${res.error || 'Unknown error'}`)
|
||||
isGeneratingReport.value = false
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`✗ 启动报告生成异常: ${err.message}`)
|
||||
addLog(`✗ Report generation exception: ${err.message}`)
|
||||
isGeneratingReport.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll log to bottom
|
||||
// Scroll log xuống cuối
|
||||
const logContent = ref(null)
|
||||
watch(() => props.systemLogs?.length, () => {
|
||||
nextTick(() => {
|
||||
|
|
@ -685,7 +685,7 @@ watch(() => props.systemLogs?.length, () => {
|
|||
})
|
||||
|
||||
onMounted(() => {
|
||||
addLog('Step3 模拟运行初始化')
|
||||
addLog('Step3 simulation run initialization')
|
||||
if (props.simulationId) {
|
||||
doStartSimulation()
|
||||
}
|
||||
|
|
@ -966,7 +966,7 @@ onUnmounted(() => {
|
|||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
background: #EAEAEA; /* Cleaner line */
|
||||
background: #EAEAEA; /* đường line gọn hơn */
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
|
|
@ -1030,7 +1030,7 @@ onUnmounted(() => {
|
|||
}
|
||||
.timeline-item.twitter .timeline-card {
|
||||
margin-left: auto;
|
||||
margin-right: 32px; /* Gap from axis */
|
||||
margin-right: 32px; /* khoảng cách tới trục */
|
||||
}
|
||||
|
||||
/* Right side (Reddit) */
|
||||
|
|
@ -1040,7 +1040,7 @@ onUnmounted(() => {
|
|||
}
|
||||
.timeline-item.reddit .timeline-card {
|
||||
margin-right: auto;
|
||||
margin-left: 32px; /* Gap from axis */
|
||||
margin-left: 32px; /* khoảng cách tới trục */
|
||||
}
|
||||
|
||||
/* Card Content Styles */
|
||||
|
|
@ -1144,7 +1144,7 @@ onUnmounted(() => {
|
|||
color: #999;
|
||||
}
|
||||
.icon-small.filled {
|
||||
color: #999; /* Keep icons neutral unless highlighted */
|
||||
color: #999; /* giữ icon trung tính nếu không cần highlight */
|
||||
}
|
||||
|
||||
.search-query {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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">Generating{{ section.title }}...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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>Chat with Report Agent</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 : 'Chat with any individual in the world' }}</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">Choose someone to talk to</div>
|
||||
<div
|
||||
v-for="(agent, idx) in profiles"
|
||||
:key="idx"
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
<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 || 'Occupation unknown' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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>Send surveys to the world</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -155,7 +155,7 @@
|
|||
<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-subtitle">Fast conversational mode for the report-generation agent, with access to 4 professional tools and full MiroFish memory</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">InsightForge Deep Attribution</div>
|
||||
<div class="tool-desc">Aligns real-world seed data with simulation state, combining Global/Local Memory for deep cross-temporal attribution analysis</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">PanoramaSearch Panorama Tracking</div>
|
||||
<div class="tool-desc">Uses graph-based breadth traversal to reconstruct event propagation paths and capture full information-flow topology</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">QuickSearch Fast Retrieval</div>
|
||||
<div class="tool-desc">An instant GraphRAG query interface with optimized indexing for quickly extracting node attributes and discrete facts</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">InterviewSubAgent Virtual Interview</div>
|
||||
<div class="tool-desc">Autonomous interviews that run parallel multi-turn conversations with simulated individuals to collect unstructured opinions and mental-state signals</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 || 'Occupation unknown' }}</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">Bio</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' ? 'Chat with the Report Agent to explore the report in depth' : 'Chat with simulated individuals to understand their viewpoints' }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -292,7 +292,7 @@
|
|||
<textarea
|
||||
v-model="chatInput"
|
||||
class="chat-input"
|
||||
placeholder="输入您的问题..."
|
||||
placeholder="Type your question..."
|
||||
@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">Select survey targets</span>
|
||||
<span class="selection-count">Selected {{ selectedAgents.size }} / {{ 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 || 'Occupation unknown' }}</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">Select all</button>
|
||||
<span class="action-divider">|</span>
|
||||
<button class="action-link" @click="clearAgentSelection">清空</button>
|
||||
<button class="action-link" @click="clearAgentSelection">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setup-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title">问卷问题</span>
|
||||
<span class="section-title">Survey question</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-model="surveyQuestion"
|
||||
class="survey-input"
|
||||
placeholder="输入您想问所有被选中对象的问题..."
|
||||
placeholder="Type the question you want to ask all selected targets..."
|
||||
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>Send survey</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">Survey Results</span>
|
||||
<span class="results-count">{{ surveyResults.length }} responses</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 || 'Occupation unknown' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="result-question">
|
||||
|
|
@ -434,7 +434,7 @@ const showToolsDetail = ref(true)
|
|||
// Chat State
|
||||
const chatInput = ref('')
|
||||
const chatHistory = ref([])
|
||||
const chatHistoryCache = ref({}) // 缓存所有对话记录: { 'report_agent': [], 'agent_0': [], 'agent_1': [], ... }
|
||||
const chatHistoryCache = ref({}) // Luu bo nho dem cho toan bo lich su hoi thoai: { 'report_agent': [], 'agent_0': [], 'agent_1': [], ... }
|
||||
const isSending = ref(false)
|
||||
const chatMessages = ref(null)
|
||||
const chatInputRef = ref(null)
|
||||
|
|
@ -484,7 +484,7 @@ const selectChatTarget = (target) => {
|
|||
}
|
||||
}
|
||||
|
||||
// 保存当前对话记录到缓存
|
||||
// Luu lich su hoi thoai hien tai vao bo nho dem
|
||||
const saveChatHistory = () => {
|
||||
if (chatHistory.value.length === 0) return
|
||||
|
||||
|
|
@ -496,7 +496,7 @@ const saveChatHistory = () => {
|
|||
}
|
||||
|
||||
const selectReportAgentChat = () => {
|
||||
// 保存当前对话记录
|
||||
// Luu lich su hoi thoai hien tai
|
||||
saveChatHistory()
|
||||
|
||||
activeTab.value = 'chat'
|
||||
|
|
@ -505,7 +505,7 @@ const selectReportAgentChat = () => {
|
|||
selectedAgentIndex.value = null
|
||||
showAgentDropdown.value = false
|
||||
|
||||
// 恢复 Report Agent 的对话记录
|
||||
// Khoi phuc lich su hoi thoai cua Report Agent
|
||||
chatHistory.value = chatHistoryCache.value['report_agent'] || []
|
||||
}
|
||||
|
||||
|
|
@ -525,7 +525,7 @@ const toggleAgentDropdown = () => {
|
|||
}
|
||||
|
||||
const selectAgent = (agent, idx) => {
|
||||
// 保存当前对话记录
|
||||
// Luu lich su hoi thoai hien tai
|
||||
saveChatHistory()
|
||||
|
||||
selectedAgent.value = agent
|
||||
|
|
@ -533,9 +533,9 @@ const selectAgent = (agent, idx) => {
|
|||
chatTarget.value = 'agent'
|
||||
showAgentDropdown.value = false
|
||||
|
||||
// 恢复该 Agent 的对话记录
|
||||
// Khoi phuc lich su hoi thoai cua Agent nay
|
||||
chatHistory.value = chatHistoryCache.value[`agent_${idx}`] || []
|
||||
addLog(`选择对话对象: ${agent.username}`)
|
||||
addLog(`Selected chat target: ${agent.username}`)
|
||||
}
|
||||
|
||||
const formatTime = (timestamp) => {
|
||||
|
|
@ -563,7 +563,7 @@ const renderMarkdown = (content) => {
|
|||
html = html.replace(/^# (.+)$/gm, '<h2 class="md-h2">$1</h2>')
|
||||
html = html.replace(/^> (.+)$/gm, '<blockquote class="md-quote">$1</blockquote>')
|
||||
|
||||
// 处理列表 - 支持子列表
|
||||
// Xu ly danh sach - ho tro danh sach con
|
||||
html = html.replace(/^(\s*)- (.+)$/gm, (match, indent, text) => {
|
||||
const level = Math.floor(indent.length / 2)
|
||||
return `<li class="md-li" data-level="${level}">${text}</li>`
|
||||
|
|
@ -573,17 +573,17 @@ const renderMarkdown = (content) => {
|
|||
return `<li class="md-oli" data-level="${level}">${text}</li>`
|
||||
})
|
||||
|
||||
// 包装无序列表
|
||||
// Bao boc danh sach khong thu tu
|
||||
html = html.replace(/(<li class="md-li"[^>]*>.*?<\/li>\s*)+/g, '<ul class="md-ul">$&</ul>')
|
||||
// 包装有序列表
|
||||
// Bao boc danh sach co thu tu
|
||||
html = html.replace(/(<li class="md-oli"[^>]*>.*?<\/li>\s*)+/g, '<ol class="md-ol">$&</ol>')
|
||||
|
||||
// 清理列表项之间的所有空白
|
||||
// Lam sach khoang trang giua cac muc trong danh sach
|
||||
html = html.replace(/<\/li>\s+<li/g, '</li><li')
|
||||
// 清理列表开始标签后的空白
|
||||
// Lam sach khoang trang sau the mo dau danh sach
|
||||
html = html.replace(/<ul class="md-ul">\s+/g, '<ul class="md-ul">')
|
||||
html = html.replace(/<ol class="md-ol">\s+/g, '<ol class="md-ol">')
|
||||
// 清理列表结束标签前的空白
|
||||
// Lam sach khoang trang truoc the dong danh sach
|
||||
html = html.replace(/\s+<\/ul>/g, '</ul>')
|
||||
html = html.replace(/\s+<\/ol>/g, '</ol>')
|
||||
|
||||
|
|
@ -599,17 +599,17 @@ const renderMarkdown = (content) => {
|
|||
html = html.replace(/(<\/h[2-5]>)<\/p>/g, '$1')
|
||||
html = html.replace(/<p class="md-p">(<ul|<ol|<blockquote|<pre|<hr)/g, '$1')
|
||||
html = html.replace(/(<\/ul>|<\/ol>|<\/blockquote>|<\/pre>)<\/p>/g, '$1')
|
||||
// 清理块级元素前后的 <br> 标签
|
||||
// Lam sach the <br> truoc va sau phan tu khoi
|
||||
html = html.replace(/<br>\s*(<ul|<ol|<blockquote)/g, '$1')
|
||||
html = html.replace(/(<\/ul>|<\/ol>|<\/blockquote>)\s*<br>/g, '$1')
|
||||
// 清理 <p><br> 紧跟块级元素的情况(多余空行导致)
|
||||
// Lam sach truong hop <p><br> dung truoc phan tu khoi (do dong trong du)
|
||||
html = html.replace(/<p class="md-p">(<br>\s*)+(<ul|<ol|<blockquote|<pre|<hr)/g, '$2')
|
||||
// 清理连续的 <br> 标签
|
||||
// Lam sach cac the <br> lien tiep
|
||||
html = html.replace(/(<br>\s*){2,}/g, '<br>')
|
||||
// 清理块级元素后紧跟的段落开始标签前的 <br>
|
||||
// Lam sach <br> truoc the mo dau doan van ngay sau phan tu khoi
|
||||
html = html.replace(/(<\/ol>|<\/ul>|<\/blockquote>)<br>(<p|<div)/g, '$1$2')
|
||||
|
||||
// 修复非连续有序列表的编号:当单项 <ol> 被段落内容隔开时,保持编号递增
|
||||
// Sua so thu tu danh sach co thu tu khong lien tuc: giu chi so tang dan khi cac muc <ol> don le bi ngan boi doan van
|
||||
const tokens = html.split(/(<ol class="md-ol">(?:<li class="md-oli"[^>]*>[\s\S]*?<\/li>)+<\/ol>)/g)
|
||||
let olCounter = 0
|
||||
let inSequence = false
|
||||
|
|
@ -662,22 +662,22 @@ const sendMessage = async () => {
|
|||
await sendToAgent(message)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`发送失败: ${err.message}`)
|
||||
addLog(`Send failed: ${err.message}`)
|
||||
chatHistory.value.push({
|
||||
role: 'assistant',
|
||||
content: `抱歉,发生了错误: ${err.message}`,
|
||||
content: `Sorry, an error occurred: ${err.message}`,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
} finally {
|
||||
isSending.value = false
|
||||
scrollToBottom()
|
||||
// 自动保存对话记录到缓存
|
||||
// Tu dong luu lich su hoi thoai vao bo nho dem
|
||||
saveChatHistory()
|
||||
}
|
||||
}
|
||||
|
||||
const sendToReportAgent = async (message) => {
|
||||
addLog(`向 Report Agent 发送: ${message.substring(0, 50)}...`)
|
||||
addLog(`Sent to Report Agent: ${message.substring(0, 50)}...`)
|
||||
|
||||
// Build chat history for API
|
||||
const historyForApi = chatHistory.value
|
||||
|
|
@ -697,21 +697,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 || 'No response',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
addLog('Report Agent 已回复')
|
||||
addLog('Report Agent replied')
|
||||
} else {
|
||||
throw new Error(res.error || '请求失败')
|
||||
throw new Error(res.error || 'Request failed')
|
||||
}
|
||||
}
|
||||
|
||||
const sendToAgent = async (message) => {
|
||||
if (!selectedAgent.value || selectedAgentIndex.value === null) {
|
||||
throw new Error('请先选择一个模拟个体')
|
||||
throw new Error('Please select a simulated individual first')
|
||||
}
|
||||
|
||||
addLog(`向 ${selectedAgent.value.username} 发送: ${message.substring(0, 50)}...`)
|
||||
addLog(`Sent to ${selectedAgent.value.username}: ${message.substring(0, 50)}...`)
|
||||
|
||||
// Build prompt with chat history
|
||||
let prompt = message
|
||||
|
|
@ -719,9 +719,9 @@ const sendToAgent = async (message) => {
|
|||
const historyContext = chatHistory.value
|
||||
.filter(msg => msg.content !== message)
|
||||
.slice(-6)
|
||||
.map(msg => `${msg.role === 'user' ? '提问者' : '你'}:${msg.content}`)
|
||||
.map(msg => `${msg.role === 'user' ? 'Questioner' : 'You'}: ${msg.content}`)
|
||||
.join('\n')
|
||||
prompt = `以下是我们之前的对话:\n${historyContext}\n\n现在我的新问题是:${message}`
|
||||
prompt = `Here is our previous conversation:\n${historyContext}\n\nMy new question is: ${message}`
|
||||
}
|
||||
|
||||
const res = await interviewAgents({
|
||||
|
|
@ -733,17 +733,17 @@ const sendToAgent = async (message) => {
|
|||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
// 正确的数据路径: res.data.result.results 是一个对象字典
|
||||
// 格式: {"twitter_0": {...}, "reddit_0": {...}} 或单平台 {"reddit_0": {...}}
|
||||
// Correct data path: res.data.result.results is an object dictionary
|
||||
// Format: {"twitter_0": {...}, "reddit_0": {...}} or single-platform {"reddit_0": {...}}
|
||||
const resultData = res.data.result || res.data
|
||||
const resultsDict = resultData.results || resultData
|
||||
|
||||
// 将对象字典转换为数组,优先获取 reddit 平台的回复
|
||||
// Convert dictionary-style results and prefer reddit response first
|
||||
let responseContent = null
|
||||
const agentId = selectedAgentIndex.value
|
||||
|
||||
if (typeof resultsDict === 'object' && !Array.isArray(resultsDict)) {
|
||||
// 优先使用 reddit 平台回复,其次 twitter
|
||||
// Prefer reddit response, then twitter
|
||||
const redditKey = `reddit_${agentId}`
|
||||
const twitterKey = `twitter_${agentId}`
|
||||
const agentResult = resultsDict[redditKey] || resultsDict[twitterKey] || Object.values(resultsDict)[0]
|
||||
|
|
@ -751,7 +751,7 @@ const sendToAgent = async (message) => {
|
|||
responseContent = agentResult.response || agentResult.answer
|
||||
}
|
||||
} else if (Array.isArray(resultsDict) && resultsDict.length > 0) {
|
||||
// 兼容数组格式
|
||||
// Backward compatible with array format
|
||||
responseContent = resultsDict[0].response || resultsDict[0].answer
|
||||
}
|
||||
|
||||
|
|
@ -761,12 +761,12 @@ const sendToAgent = async (message) => {
|
|||
content: responseContent,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
addLog(`${selectedAgent.value.username} 已回复`)
|
||||
addLog(`${selectedAgent.value.username} replied`)
|
||||
} else {
|
||||
throw new Error('无响应数据')
|
||||
throw new Error('No response data')
|
||||
}
|
||||
} else {
|
||||
throw new Error(res.error || '请求失败')
|
||||
throw new Error(res.error || 'Request failed')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -803,7 +803,7 @@ const submitSurvey = async () => {
|
|||
if (selectedAgents.value.size === 0 || !surveyQuestion.value.trim()) return
|
||||
|
||||
isSurveying.value = true
|
||||
addLog(`发送问卷给 ${selectedAgents.value.size} 个对象...`)
|
||||
addLog(`Sending survey to ${selectedAgents.value.size} targets...`)
|
||||
|
||||
try {
|
||||
const interviews = Array.from(selectedAgents.value).map(idx => ({
|
||||
|
|
@ -817,33 +817,33 @@ const submitSurvey = async () => {
|
|||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
// 正确的数据路径: res.data.result.results 是一个对象字典
|
||||
// 格式: {"twitter_0": {...}, "reddit_0": {...}, "twitter_1": {...}, ...}
|
||||
// Correct data path: res.data.result.results is an object dictionary
|
||||
// Format: {"twitter_0": {...}, "reddit_0": {...}, "twitter_1": {...}, ...}
|
||||
const resultData = res.data.result || res.data
|
||||
const resultsDict = resultData.results || resultData
|
||||
|
||||
// 将对象字典转换为数组格式
|
||||
// Convert object dictionary to array format
|
||||
const surveyResultsList = []
|
||||
|
||||
for (const interview of interviews) {
|
||||
const agentIdx = interview.agent_id
|
||||
const agent = profiles.value[agentIdx]
|
||||
|
||||
// 优先使用 reddit 平台回复,其次 twitter
|
||||
let responseContent = '无响应'
|
||||
// Prefer reddit response, then twitter
|
||||
let responseContent = 'No response'
|
||||
|
||||
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 || 'No response'
|
||||
}
|
||||
} else if (Array.isArray(resultsDict)) {
|
||||
// 兼容数组格式
|
||||
// Backward compatible with array format
|
||||
const matchedResult = resultsDict.find(r => r.agent_id === agentIdx)
|
||||
if (matchedResult) {
|
||||
responseContent = matchedResult.response || matchedResult.answer || '无响应'
|
||||
responseContent = matchedResult.response || matchedResult.answer || 'No response'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -857,12 +857,12 @@ const submitSurvey = async () => {
|
|||
}
|
||||
|
||||
surveyResults.value = surveyResultsList
|
||||
addLog(`收到 ${surveyResults.value.length} 条回复`)
|
||||
addLog(`Received ${surveyResults.value.length} responses`)
|
||||
} else {
|
||||
throw new Error(res.error || '请求失败')
|
||||
throw new Error(res.error || 'Request failed')
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`问卷发送失败: ${err.message}`)
|
||||
addLog(`Survey send failed: ${err.message}`)
|
||||
} finally {
|
||||
isSurveying.value = false
|
||||
}
|
||||
|
|
@ -873,7 +873,7 @@ const loadReportData = async () => {
|
|||
if (!props.reportId) return
|
||||
|
||||
try {
|
||||
addLog(`加载报告数据: ${props.reportId}`)
|
||||
addLog(`Loading report data: ${props.reportId}`)
|
||||
|
||||
// Get report info
|
||||
const reportRes = await getReport(props.reportId)
|
||||
|
|
@ -882,7 +882,7 @@ const loadReportData = async () => {
|
|||
await loadAgentLogs()
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载报告失败: ${err.message}`)
|
||||
addLog(`Failed to load report: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -904,10 +904,10 @@ const loadAgentLogs = async () => {
|
|||
}
|
||||
})
|
||||
|
||||
addLog('报告数据加载完成')
|
||||
addLog('Report data loaded')
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载报告日志失败: ${err.message}`)
|
||||
addLog(`Failed to load report logs: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -918,10 +918,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(`Loaded ${profiles.value.length} simulated individuals`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载模拟个体失败: ${err.message}`)
|
||||
addLog(`Failed to load simulated individuals: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -935,7 +935,7 @@ const handleClickOutside = (e) => {
|
|||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
addLog('Step5 深度互动初始化')
|
||||
addLog('Step5 deep interaction initialized')
|
||||
loadReportData()
|
||||
loadProfiles()
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
|
|
@ -980,7 +980,7 @@ watch(() => props.simulationId, (newId) => {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Left Panel - Report Style (与 Step4Report.vue 完全一致) */
|
||||
/* Left Panel - Report Style (identical to Step4Report.vue) */
|
||||
.left-panel.report-style {
|
||||
width: 45%;
|
||||
min-width: 450px;
|
||||
|
|
@ -2028,7 +2028,7 @@ watch(() => props.simulationId, (newId) => {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* 修复有序列表编号 - 使用 CSS 计数器让多个 ol 连续编号 */
|
||||
/* Fix ordered-list numbering - use CSS counter to keep numbering continuous across multiple ol blocks */
|
||||
.message-text {
|
||||
counter-reset: list-counter;
|
||||
}
|
||||
|
|
@ -2054,7 +2054,7 @@ watch(() => props.simulationId, (newId) => {
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 无序列表样式 */
|
||||
/* Unordered list styles */
|
||||
.message-text :deep(.md-ul) {
|
||||
padding-left: 20px;
|
||||
margin: 8px 0;
|
||||
|
|
@ -2533,7 +2533,7 @@ watch(() => props.simulationId, (newId) => {
|
|||
margin: 6px 0;
|
||||
}
|
||||
|
||||
/* 聊天/问卷区域的引用样式 */
|
||||
/* Quote styles for chat/survey area */
|
||||
.chat-messages :deep(.md-quote),
|
||||
.result-answer :deep(.md-quote) {
|
||||
margin: 12px 0;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* 临时存储待上传的文件和需求
|
||||
* 用于首页点击启动引擎后立即跳转,在Process页面再进行API调用
|
||||
* Lưu tạm file cần upload và yêu cầu
|
||||
* Dùng để chuyển trang ngay sau khi nhấn “khởi động engine” ở trang chủ,
|
||||
* sau đó mới gọi API ở trang Process
|
||||
*/
|
||||
import { reactive } from 'vue'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,35 +1,35 @@
|
|||
<template>
|
||||
<div class="home-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<!-- Top Navigation Bar -->
|
||||
<nav class="navbar">
|
||||
<div class="nav-brand">MIROFISH</div>
|
||||
<div class="nav-links">
|
||||
<a href="https://github.com/666ghj/MiroFish" target="_blank" class="github-link">
|
||||
访问我们的Github主页 <span class="arrow">↗</span>
|
||||
Visit our GitHub page <span class="arrow">↗</span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="main-content">
|
||||
<!-- 上半部分:Hero 区域 -->
|
||||
<!-- Upper Section: Hero Area -->
|
||||
<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">A concise and general-purpose collective intelligence engine</span>
|
||||
<span class="version-text">/ v0.1 - Preview Version</span>
|
||||
</div>
|
||||
|
||||
<h1 class="main-title">
|
||||
上传任意报告<br>
|
||||
<span class="gradient-text">即刻推演未来</span>
|
||||
Upload any report<br>
|
||||
<span class="gradient-text">Instantly simulate the future</span>
|
||||
</h1>
|
||||
|
||||
<div class="hero-desc">
|
||||
<p>
|
||||
即使只有一段文字,<span class="highlight-bold">MiroFish</span> 也能基于其中的现实种子,全自动生成与之对应的至多<span class="highlight-orange">百万级Agent</span>构成的平行世界。通过上帝视角注入变量,在复杂的群体交互中寻找动态环境下的<span class="highlight-code">“局部最优解”</span>
|
||||
Even with just a piece of text, <span class="highlight-bold">MiroFish</span> can extract real-world signals and automatically generate a parallel world with up to <span class="highlight-orange">millions of agents</span>. By injecting variables from a god-like perspective, it explores <span class="highlight-code">"locally optimal solutions"</span> within complex multi-agent interactions in dynamic environments.
|
||||
</p>
|
||||
<p class="slogan-text">
|
||||
让未来在 Agent 群中预演,让决策在百战后胜出<span class="blinking-cursor">_</span>
|
||||
Let the future be rehearsed within agent swarms, and let decisions emerge after countless simulations<span class="blinking-cursor">_</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
</div>
|
||||
|
||||
<div class="hero-right">
|
||||
<!-- Logo 区域 -->
|
||||
<!-- Logo Area -->
|
||||
<div class="logo-container">
|
||||
<img src="../assets/logo/MiroFish_logo_left.jpeg" alt="MiroFish Logo" class="hero-logo" />
|
||||
</div>
|
||||
|
|
@ -48,84 +48,84 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 下半部分:双栏布局 -->
|
||||
<!-- Lower Section: Two-column Layout -->
|
||||
<section class="dashboard-section">
|
||||
<!-- 左栏:状态与步骤 -->
|
||||
<!-- Left Column: Status and Steps -->
|
||||
<div class="left-panel">
|
||||
<div class="panel-header">
|
||||
<span class="status-dot">■</span> 系统状态
|
||||
<span class="status-dot">■</span> System Status
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">准备就绪</h2>
|
||||
<h2 class="section-title">Ready</h2>
|
||||
<p class="section-desc">
|
||||
预测引擎待命中,可上传多份非结构化数据以初始化模拟序列
|
||||
The prediction engine is on standby. Multiple unstructured data files can be uploaded to initialize the simulation sequence.
|
||||
</p>
|
||||
|
||||
<!-- 数据指标卡片 -->
|
||||
<!-- Data Metric Cards -->
|
||||
<div class="metrics-row">
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">低成本</div>
|
||||
<div class="metric-label">常规模拟平均5$/次</div>
|
||||
<div class="metric-value">Low Cost</div>
|
||||
<div class="metric-label">Average cost: $5 per simulation</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="metric-value">高可用</div>
|
||||
<div class="metric-label">最多百万级Agent模拟</div>
|
||||
<div class="metric-value">High Availability</div>
|
||||
<div class="metric-label">Supports simulations with up to millions of agents</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目模拟步骤介绍 (新增区域) -->
|
||||
<!-- Project Simulation Workflow (Added Section) -->
|
||||
<div class="steps-container">
|
||||
<div class="steps-header">
|
||||
<span class="diamond-icon">◇</span> 工作流序列
|
||||
<span class="diamond-icon">◇</span> Workflow Sequence
|
||||
</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">Graph Construction</div>
|
||||
<div class="step-desc">Real-world seed extraction & individual and collective memory injection & GraphRAG construction</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">Environment Setup</div>
|
||||
<div class="step-desc">Entity relationship extraction & persona generation & environment configuration with agent-injected simulation parameters</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">Start Simulation</div>
|
||||
<div class="step-desc">Dual-platform parallel simulation & automatic parsing of prediction requirements & dynamic temporal memory updates</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">Report Generation</div>
|
||||
<div class="step-desc">ReportAgent uses a rich toolset to deeply interact with the post-simulation environment</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">Deep Interaction</div>
|
||||
<div class="step-desc">Converse with any entity in the simulated world & interact with the ReportAgent</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右栏:交互控制台 -->
|
||||
<!-- Right Column: Interactive Console -->
|
||||
<div class="right-panel">
|
||||
<div class="console-box">
|
||||
<!-- 上传区域 -->
|
||||
<!-- Upload Area -->
|
||||
<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">01 / Real-world Seeds</span>
|
||||
<span class="console-meta">Supported formats: PDF, MD, TXT</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -148,8 +148,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">Drag files to upload</div>
|
||||
<div class="upload-hint">Or click to browse the file system</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="file-list">
|
||||
|
|
@ -162,37 +162,37 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分割线 -->
|
||||
<!-- Divider -->
|
||||
<div class="console-divider">
|
||||
<span>输入参数</span>
|
||||
<span>Input Parameters</span>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<!-- Input Area -->
|
||||
<div class="console-section">
|
||||
<div class="console-header">
|
||||
<span class="console-label">>_ 02 / 模拟提示词</span>
|
||||
<span class="console-label">>_ 02 / Simulation Prompt</span>
|
||||
</div>
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
v-model="formData.simulationRequirement"
|
||||
class="code-input"
|
||||
placeholder="// 用自然语言输入模拟或预测需求(例.武大若发布撤销肖某处分的公告,会引发什么舆情走向)"
|
||||
placeholder="// Describe your simulation or prediction request in natural language (e.g. If Wuhan University announces the revocation of disciplinary action against someone, what public opinion trends would emerge?)"
|
||||
rows="6"
|
||||
:disabled="loading"
|
||||
></textarea>
|
||||
<div class="model-badge">引擎: MiroFish-V1.0</div>
|
||||
<div class="model-badge">Engine: MiroFish-V1.0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 启动按钮 -->
|
||||
<!-- Start Button -->
|
||||
<div class="console-section btn-section">
|
||||
<button
|
||||
class="start-engine-btn"
|
||||
@click="startSimulation"
|
||||
:disabled="!canSubmit || loading"
|
||||
>
|
||||
<span v-if="!loading">启动引擎</span>
|
||||
<span v-else>初始化中...</span>
|
||||
<span v-if="!loading">Start Engine</span>
|
||||
<span v-else>Initializing...</span>
|
||||
<span class="btn-arrow">→</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -200,7 +200,7 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 历史项目数据库 -->
|
||||
<!-- Historical Project Database -->
|
||||
<HistoryDatabase />
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -213,41 +213,41 @@ import HistoryDatabase from '../components/HistoryDatabase.vue'
|
|||
|
||||
const router = useRouter()
|
||||
|
||||
// 表单数据
|
||||
// Form data
|
||||
const formData = ref({
|
||||
simulationRequirement: ''
|
||||
})
|
||||
|
||||
// 文件列表
|
||||
// File list
|
||||
const files = ref([])
|
||||
|
||||
// 状态
|
||||
// State
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
const isDragOver = ref(false)
|
||||
|
||||
// 文件输入引用
|
||||
// File input reference
|
||||
const fileInput = ref(null)
|
||||
|
||||
// 计算属性:是否可以提交
|
||||
// Computed property: whether submission is allowed
|
||||
const canSubmit = computed(() => {
|
||||
return formData.value.simulationRequirement.trim() !== '' && files.value.length > 0
|
||||
})
|
||||
|
||||
// 触发文件选择
|
||||
// Trigger file selection
|
||||
const triggerFileInput = () => {
|
||||
if (!loading.value) {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
// Handle file selection
|
||||
const handleFileSelect = (event) => {
|
||||
const selectedFiles = Array.from(event.target.files)
|
||||
addFiles(selectedFiles)
|
||||
}
|
||||
|
||||
// 处理拖拽相关
|
||||
// Handle drag-related events
|
||||
const handleDragOver = (e) => {
|
||||
if (!loading.value) {
|
||||
isDragOver.value = true
|
||||
|
|
@ -266,7 +266,7 @@ const handleDrop = (e) => {
|
|||
addFiles(droppedFiles)
|
||||
}
|
||||
|
||||
// 添加文件
|
||||
// Add files
|
||||
const addFiles = (newFiles) => {
|
||||
const validFiles = newFiles.filter(file => {
|
||||
const ext = file.name.split('.').pop().toLowerCase()
|
||||
|
|
@ -275,12 +275,12 @@ const addFiles = (newFiles) => {
|
|||
files.value.push(...validFiles)
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
// Remove file
|
||||
const removeFile = (index) => {
|
||||
files.value.splice(index, 1)
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
// Scroll to bottom
|
||||
const scrollToBottom = () => {
|
||||
window.scrollTo({
|
||||
top: document.body.scrollHeight,
|
||||
|
|
@ -288,15 +288,15 @@ const scrollToBottom = () => {
|
|||
})
|
||||
}
|
||||
|
||||
// 开始模拟 - 立即跳转,API调用在Process页面进行
|
||||
// Start simulation - immediate redirection, API call handled on Process page
|
||||
const startSimulation = () => {
|
||||
if (!canSubmit.value || loading.value) return
|
||||
|
||||
// 存储待上传的数据
|
||||
// Store pending upload data
|
||||
import('../store/pendingUpload.js').then(({ setPendingUpload }) => {
|
||||
setPendingUpload(files.value, formData.value.simulationRequirement)
|
||||
|
||||
// 立即跳转到Process页面(使用特殊标识表示新建项目)
|
||||
// Immediately redirect to Process page (using special marker for new project)
|
||||
router.push({
|
||||
name: 'Process',
|
||||
params: { projectId: 'new' }
|
||||
|
|
@ -306,7 +306,7 @@ const startSimulation = () => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 全局变量与重置 */
|
||||
/* Global variables and reset */
|
||||
:root {
|
||||
--black: #000000;
|
||||
--white: #FFFFFF;
|
||||
|
|
@ -315,8 +315,8 @@ const startSimulation = () => {
|
|||
--gray-text: #666666;
|
||||
--border: #E5E5E5;
|
||||
/*
|
||||
使用 Space Grotesk 作为主要标题字体,JetBrains Mono 作为代码/标签字体
|
||||
确保已在 index.html 引入这些 Google Fonts
|
||||
Use Space Grotesk as the primary heading font, and JetBrains Mono as the code/tag font.
|
||||
Make sure these Google Fonts are imported in index.html.
|
||||
*/
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
--font-sans: 'Space Grotesk', 'Noto Sans SC', system-ui, sans-serif;
|
||||
|
|
@ -330,7 +330,7 @@ const startSimulation = () => {
|
|||
color: var(--black);
|
||||
}
|
||||
|
||||
/* 顶部导航 */
|
||||
/* Top navigation */
|
||||
.navbar {
|
||||
height: 60px;
|
||||
background: var(--black);
|
||||
|
|
@ -373,14 +373,14 @@ const startSimulation = () => {
|
|||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
/* 主要内容区 */
|
||||
/* Main content area */
|
||||
.main-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 60px 40px;
|
||||
}
|
||||
|
||||
/* Hero 区域 */
|
||||
/* Hero area */
|
||||
.hero-section {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
|
@ -511,7 +511,7 @@ const startSimulation = () => {
|
|||
}
|
||||
|
||||
.hero-logo {
|
||||
max-width: 500px; /* 调整logo大小 */
|
||||
max-width: 500px; /* Adjust logo size */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -533,7 +533,7 @@ const startSimulation = () => {
|
|||
border-color: var(--orange);
|
||||
}
|
||||
|
||||
/* Dashboard 双栏布局 */
|
||||
/* Dashboard two-column layout */
|
||||
.dashboard-section {
|
||||
display: flex;
|
||||
gap: 60px;
|
||||
|
|
@ -548,7 +548,7 @@ const startSimulation = () => {
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 左侧面板 */
|
||||
/* Left panel */
|
||||
.left-panel {
|
||||
flex: 0.8;
|
||||
}
|
||||
|
|
@ -604,7 +604,7 @@ const startSimulation = () => {
|
|||
color: #999;
|
||||
}
|
||||
|
||||
/* 项目模拟步骤介绍 */
|
||||
/* Project simulation steps introduction */
|
||||
.steps-container {
|
||||
border: 1px solid var(--border);
|
||||
padding: 30px;
|
||||
|
|
@ -660,14 +660,14 @@ const startSimulation = () => {
|
|||
color: var(--gray-text);
|
||||
}
|
||||
|
||||
/* 右侧交互控制台 */
|
||||
/* Right interactive console */
|
||||
.right-panel {
|
||||
flex: 1.2;
|
||||
}
|
||||
|
||||
.console-box {
|
||||
border: 1px solid #CCC; /* 外部实线 */
|
||||
padding: 8px; /* 内边距形成双重边框感 */
|
||||
border: 1px solid #CCC; /* Outer solid border */
|
||||
padding: 8px; /* Padding creates a double-border effect */
|
||||
}
|
||||
|
||||
.console-section {
|
||||
|
|
@ -835,7 +835,7 @@ const startSimulation = () => {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 可点击状态(非禁用) */
|
||||
/* Clickable (not disabled) */
|
||||
.start-engine-btn:not(:disabled) {
|
||||
background: var(--black);
|
||||
border: 1px solid var(--black);
|
||||
|
|
@ -860,14 +860,14 @@ const startSimulation = () => {
|
|||
border: 1px solid #E5E5E5;
|
||||
}
|
||||
|
||||
/* 引导动画:微妙的边框脉冲 */
|
||||
/* Guided animation: subtle border pulses */
|
||||
@keyframes pulse-border {
|
||||
0% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.2); }
|
||||
70% { box-shadow: 0 0 0 6px rgba(0, 0, 0, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
|
||||
}
|
||||
|
||||
/* 响应式适配 */
|
||||
/* Responsive adaptation */
|
||||
@media (max-width: 1024px) {
|
||||
.dashboard-section {
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: 'Graph', split: 'Split', workbench: 'Workbench' }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<div class="header-right">
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 5/5</span>
|
||||
<span class="step-name">深度互动</span>
|
||||
<span class="step-name">Deep Interaction</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Step5 深度互动 -->
|
||||
<!-- Right Panel: Step5 Deep Interaction -->
|
||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||
<Step5Interaction
|
||||
:reportId="currentReportId"
|
||||
|
|
@ -78,7 +78,7 @@ const props = defineProps({
|
|||
reportId: String
|
||||
})
|
||||
|
||||
// Layout State - 默认切换到工作台视角
|
||||
// Layout State - Default to Workbench view
|
||||
const viewMode = ref('workbench')
|
||||
|
||||
// Data State
|
||||
|
|
@ -140,28 +140,28 @@ const toggleMaximize = (target) => {
|
|||
// --- Data Logic ---
|
||||
const loadReportData = async () => {
|
||||
try {
|
||||
addLog(`加载报告数据: ${currentReportId.value}`)
|
||||
addLog(`Loading report data: ${currentReportId.value}`)
|
||||
|
||||
// 获取 report 信息以获取 simulation_id
|
||||
// Get report information to obtain 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 信息
|
||||
// Get simulation information
|
||||
const simRes = await getSimulation(simulationId.value)
|
||||
if (simRes.success && simRes.data) {
|
||||
const simData = simRes.data
|
||||
|
||||
// 获取 project 信息
|
||||
|
||||
// Get project information
|
||||
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}`)
|
||||
|
||||
// 获取 graph 数据
|
||||
addLog(`Project loaded successfully: ${projRes.data.project_id}`)
|
||||
|
||||
// Get graph data
|
||||
if (projRes.data.graph_id) {
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
}
|
||||
|
|
@ -170,10 +170,10 @@ const loadReportData = async () => {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`获取报告信息失败: ${reportRes.error || '未知错误'}`)
|
||||
addLog(`Failed to fetch report: ${reportRes.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(`Load error: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -184,10 +184,10 @@ const loadGraph = async (graphId) => {
|
|||
const res = await getGraphData(graphId)
|
||||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
addLog('图谱数据加载成功')
|
||||
addLog('Graph data loaded successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(`Graph load failed: ${err.message}`)
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
|
|
@ -208,7 +208,7 @@ watch(() => route.params.reportId, (newId) => {
|
|||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
addLog('InteractionView 初始化')
|
||||
addLog('InteractionView initialized')
|
||||
loadReportData()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
<!-- Right Panel: Step Components -->
|
||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||
<!-- Step 1: 图谱构建 -->
|
||||
<!-- Step 1: Build graph -->
|
||||
<Step1GraphBuild
|
||||
v-if="currentStep === 1"
|
||||
:currentPhase="currentPhase"
|
||||
|
|
@ -59,7 +59,7 @@
|
|||
:systemLogs="systemLogs"
|
||||
@next-step="handleNextStep"
|
||||
/>
|
||||
<!-- Step 2: 环境搭建 -->
|
||||
<!-- Step 2: Thiết lập môi trường -->
|
||||
<Step2EnvSetup
|
||||
v-else-if="currentStep === 2"
|
||||
:projectData="projectData"
|
||||
|
|
@ -90,8 +90,8 @@ const router = useRouter()
|
|||
const viewMode = ref('split') // graph | split | workbench
|
||||
|
||||
// Step State
|
||||
const currentStep = ref(1) // 1: 图谱构建, 2: 环境搭建, 3: 开始模拟, 4: 报告生成, 5: 深度互动
|
||||
const stepNames = ['图谱构建', '环境搭建', '开始模拟', '报告生成', '深度互动']
|
||||
const currentStep = ref(1) // 1: Xây dựng đồ thị, 2: Thiết lập môi trường, 3: Bắt đầu mô phỏng, 4: Tạo báo cáo, 5: Tương tác sâu
|
||||
const stepNames = ['Graph Construction', 'Environment Setup', 'Start Simulation', 'Report Generation', 'Deep Interaction']
|
||||
|
||||
// Data State
|
||||
const currentProjectId = ref(route.params.projectId)
|
||||
|
|
@ -159,11 +159,11 @@ const toggleMaximize = (target) => {
|
|||
const handleNextStep = (params = {}) => {
|
||||
if (currentStep.value < 5) {
|
||||
currentStep.value++
|
||||
addLog(`进入 Step ${currentStep.value}: ${stepNames[currentStep.value - 1]}`)
|
||||
addLog(`Entering Step ${currentStep.value}: ${stepNames[currentStep.value - 1]}`)
|
||||
|
||||
// 如果是从 Step 2 进入 Step 3,记录模拟轮数配置
|
||||
// Nếu chuyển từ Step 2 sang Step 3, ghi lại cấu hình số vòng mô phỏng
|
||||
if (currentStep.value === 3 && params.maxRounds) {
|
||||
addLog(`自定义模拟轮数: ${params.maxRounds} 轮`)
|
||||
addLog(`Custom simulation rounds: ${params.maxRounds} rounds`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -15,7 +15,7 @@
|
|||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: 'Graph', split: 'Split', workbench: 'Workbench' }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<div class="header-right">
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 4/5</span>
|
||||
<span class="step-name">报告生成</span>
|
||||
<span class="step-name">Report Generation</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Step4 报告生成 -->
|
||||
<!-- Right Panel: Step4 Report Generation -->
|
||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||
<Step4Report
|
||||
:reportId="currentReportId"
|
||||
|
|
@ -78,7 +78,7 @@ const props = defineProps({
|
|||
reportId: String
|
||||
})
|
||||
|
||||
// Layout State - 默认切换到工作台视角
|
||||
// Layout State - Default to Workbench view
|
||||
const viewMode = ref('workbench')
|
||||
|
||||
// Data State
|
||||
|
|
@ -139,28 +139,27 @@ const toggleMaximize = (target) => {
|
|||
// --- Data Logic ---
|
||||
const loadReportData = async () => {
|
||||
try {
|
||||
addLog(`加载报告数据: ${currentReportId.value}`)
|
||||
addLog(`Loading report data: ${currentReportId.value}`)
|
||||
|
||||
// 获取 report 信息以获取 simulation_id
|
||||
// Get report information to obtain 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 信息
|
||||
// Get simulation information
|
||||
const simRes = await getSimulation(simulationId.value)
|
||||
if (simRes.success && simRes.data) {
|
||||
const simData = simRes.data
|
||||
|
||||
// 获取 project 信息
|
||||
// Get project information
|
||||
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}`)
|
||||
|
||||
// 获取 graph 数据
|
||||
addLog(`Project loaded successfully: ${projRes.data.project_id}`)
|
||||
|
||||
// Get graph data
|
||||
if (projRes.data.graph_id) {
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
}
|
||||
|
|
@ -169,10 +168,10 @@ const loadReportData = async () => {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`获取报告信息失败: ${reportRes.error || '未知错误'}`)
|
||||
addLog(`Failed to fetch report: ${reportRes.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(`Load error: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,10 +182,10 @@ const loadGraph = async (graphId) => {
|
|||
const res = await getGraphData(graphId)
|
||||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
addLog('图谱数据加载成功')
|
||||
addLog('Graph data loaded successfully')
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(`Graph load failed: ${err.message}`)
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
|
|
@ -207,7 +206,7 @@ watch(() => route.params.reportId, (newId) => {
|
|||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
addLog('ReportView 初始化')
|
||||
addLog('ReportView initialized')
|
||||
loadReportData()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: 'Graph', split: 'Split', workbench: 'Workbench' }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<div class="header-right">
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 3/5</span>
|
||||
<span class="step-name">开始模拟</span>
|
||||
<span class="step-name">Start Simulation</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Step3 开始模拟 -->
|
||||
<!-- Right Panel: Step3 Start Simulation -->
|
||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||
<Step3Simulation
|
||||
:simulationId="currentSimulationId"
|
||||
|
|
@ -87,9 +87,9 @@ const viewMode = ref('split')
|
|||
|
||||
// Data State
|
||||
const currentSimulationId = ref(route.params.simulationId)
|
||||
// 直接在初始化时从 query 参数获取 maxRounds,确保子组件能立即获取到值
|
||||
// Get maxRounds directly from the query parameter during initialization to ensure that child components can immediately access the value.
|
||||
const maxRounds = ref(route.query.maxRounds ? parseInt(route.query.maxRounds) : null)
|
||||
const minutesPerRound = ref(30) // 默认每轮30分钟
|
||||
const minutesPerRound = ref(30) // The default round lasts 30 minutes.
|
||||
const projectData = ref(null)
|
||||
const graphData = ref(null)
|
||||
const graphLoading = ref(false)
|
||||
|
|
@ -145,104 +145,102 @@ const toggleMaximize = (target) => {
|
|||
}
|
||||
|
||||
const handleGoBack = async () => {
|
||||
// 在返回 Step 2 之前,先关闭正在运行的模拟
|
||||
addLog('准备返回 Step 2,正在关闭模拟...')
|
||||
|
||||
// 停止轮询
|
||||
// Trước khi quay lại Step 2, cần đóng mô phỏng đang chạy
|
||||
addLog('Preparing to return to Step 2, shutting down simulation...')
|
||||
|
||||
// Dừng việc polling
|
||||
stopGraphRefresh()
|
||||
|
||||
try {
|
||||
// 先尝试优雅关闭模拟环境
|
||||
// Trước tiên, thử đóng môi trường mô phỏng một cách an toàn (gracefully)
|
||||
const envStatusRes = await getEnvStatus({ simulation_id: currentSimulationId.value })
|
||||
|
||||
|
||||
if (envStatusRes.success && envStatusRes.data?.env_alive) {
|
||||
addLog('正在关闭模拟环境...')
|
||||
addLog('Closing simulation environment...')
|
||||
try {
|
||||
await closeSimulationEnv({
|
||||
simulation_id: currentSimulationId.value,
|
||||
timeout: 10
|
||||
})
|
||||
addLog('✓ 模拟环境已关闭')
|
||||
addLog('✓ Simulation environment closed')
|
||||
} catch (closeErr) {
|
||||
addLog(`关闭模拟环境失败,尝试强制停止...`)
|
||||
addLog(`Failed to close simulation environment, attempting force stop...`)
|
||||
try {
|
||||
await stopSimulation({ simulation_id: currentSimulationId.value })
|
||||
addLog('✓ 模拟已强制停止')
|
||||
addLog('✓ The simulation has been forcibly stopped')
|
||||
} catch (stopErr) {
|
||||
addLog(`强制停止失败: ${stopErr.message}`)
|
||||
addLog(`Forced stop failed: ${stopErr.message}`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 环境未运行,检查是否需要停止进程
|
||||
// Nếu môi trường không chạy, kiểm tra xem có cần dừng tiến trình hay không
|
||||
if (isSimulating.value) {
|
||||
addLog('正在停止模拟进程...')
|
||||
addLog('Stopping the simulation process...')
|
||||
try {
|
||||
await stopSimulation({ simulation_id: currentSimulationId.value })
|
||||
addLog('✓ 模拟已停止')
|
||||
addLog('✓ Simulation has stopped')
|
||||
} catch (err) {
|
||||
addLog(`停止模拟失败: ${err.message}`)
|
||||
addLog(`Stop simulation failed: ${err.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`检查模拟状态失败: ${err.message}`)
|
||||
addLog(`Simulation status check failed: ${err.message}`)
|
||||
}
|
||||
|
||||
// 返回到 Step 2 (环境搭建)
|
||||
|
||||
// Return to Step 2 (Environment Setup)
|
||||
router.push({ name: 'Simulation', params: { simulationId: currentSimulationId.value } })
|
||||
}
|
||||
|
||||
const handleNextStep = () => {
|
||||
// Step3Simulation 组件会直接处理报告生成和路由跳转
|
||||
// 这个方法仅作为备用
|
||||
addLog('进入 Step 4: 报告生成')
|
||||
addLog('Entering Step 4: Report Generation')
|
||||
}
|
||||
|
||||
// --- Data Logic ---
|
||||
const loadSimulationData = async () => {
|
||||
try {
|
||||
addLog(`加载模拟数据: ${currentSimulationId.value}`)
|
||||
|
||||
// 获取 simulation 信息
|
||||
addLog(`Loading simulation data: ${currentSimulationId.value}`)
|
||||
|
||||
// Get simulation information
|
||||
const simRes = await getSimulation(currentSimulationId.value)
|
||||
if (simRes.success && simRes.data) {
|
||||
const simData = simRes.data
|
||||
|
||||
// 获取 simulation config 以获取 minutes_per_round
|
||||
// Get simulation config to get minutes_per_round
|
||||
try {
|
||||
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(`Time configuration: ${minutesPerRound.value} minutes per round`)
|
||||
}
|
||||
} catch (configErr) {
|
||||
addLog(`获取时间配置失败,使用默认值: ${minutesPerRound.value}分钟/轮`)
|
||||
addLog(`Failed to fetch time configuration, using default value: ${minutesPerRound.value} minutes per round`)
|
||||
}
|
||||
|
||||
// 获取 project 信息
|
||||
// Get project information
|
||||
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(`Project loaded successfully: ${projRes.data.project_id}`)
|
||||
|
||||
// 获取 graph 数据
|
||||
// Get graph data
|
||||
if (projRes.data.graph_id) {
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`加载模拟数据失败: ${simRes.error || '未知错误'}`)
|
||||
addLog(`Failed to load simulation data: ${simRes.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(`Load error: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const loadGraph = async (graphId) => {
|
||||
// 当正在模拟时,自动刷新不显示全屏 loading,以免闪烁
|
||||
// 手动刷新或初始加载时显示 loading
|
||||
// Khi đang chạy mô phỏng, auto-refresh sẽ không hiển thị loading toàn màn hình để tránh bị nhấp nháy
|
||||
// Chỉ hiển thị loading khi refresh thủ công hoặc khi tải lần đầu
|
||||
if (!isSimulating.value) {
|
||||
graphLoading.value = true
|
||||
}
|
||||
|
|
@ -252,11 +250,11 @@ const loadGraph = async (graphId) => {
|
|||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
if (!isSimulating.value) {
|
||||
addLog('图谱数据加载成功')
|
||||
addLog('Map data loaded successfully')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(`Failed to load map: ${err.message}`)
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
|
|
@ -268,13 +266,13 @@ const refreshGraph = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Auto Refresh Logic ---
|
||||
// Auto refresh
|
||||
let graphRefreshTimer = null
|
||||
|
||||
const startGraphRefresh = () => {
|
||||
if (graphRefreshTimer) return
|
||||
addLog('开启图谱实时刷新 (30s)')
|
||||
// 立即刷新一次,然后每30秒刷新
|
||||
addLog('Starting real-time graph refresh (30s)')
|
||||
// Refresh immediately, then every 30 seconds
|
||||
graphRefreshTimer = setInterval(refreshGraph, 30000)
|
||||
}
|
||||
|
||||
|
|
@ -282,7 +280,7 @@ const stopGraphRefresh = () => {
|
|||
if (graphRefreshTimer) {
|
||||
clearInterval(graphRefreshTimer)
|
||||
graphRefreshTimer = null
|
||||
addLog('停止图谱实时刷新')
|
||||
addLog('Stop the real-time refresh of the graph')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -295,13 +293,7 @@ watch(isSimulating, (newValue) => {
|
|||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
addLog('SimulationRunView 初始化')
|
||||
|
||||
// 记录 maxRounds 配置(值已在初始化时从 query 参数获取)
|
||||
if (maxRounds.value) {
|
||||
addLog(`自定义模拟轮数: ${maxRounds.value}`)
|
||||
}
|
||||
|
||||
addLog('SimulationRunView initialized')
|
||||
loadSimulationData()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
:class="{ active: viewMode === mode }"
|
||||
@click="viewMode = mode"
|
||||
>
|
||||
{{ { graph: '图谱', split: '双栏', workbench: '工作台' }[mode] }}
|
||||
{{ { graph: 'Graph', split: 'Split', workbench: 'Workbench' }[mode] }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
<div class="header-right">
|
||||
<div class="workflow-step">
|
||||
<span class="step-num">Step 2/5</span>
|
||||
<span class="step-name">环境搭建</span>
|
||||
<span class="step-name">Environment Setup</span>
|
||||
</div>
|
||||
<div class="step-divider"></div>
|
||||
<span class="status-indicator" :class="statusClass">
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Right Panel: Step2 环境搭建 -->
|
||||
<!-- Right Panel: Step2 Environment Setup -->
|
||||
<div class="panel-wrapper right" :style="rightPanelStyle">
|
||||
<Step2EnvSetup
|
||||
:simulationId="currentSimulationId"
|
||||
|
|
@ -137,7 +137,7 @@ const toggleMaximize = (target) => {
|
|||
}
|
||||
|
||||
const handleGoBack = () => {
|
||||
// 返回到 process 页面
|
||||
// Quay lai trang process
|
||||
if (projectData.value?.project_id) {
|
||||
router.push({ name: 'Process', params: { projectId: projectData.value.project_id } })
|
||||
} else {
|
||||
|
|
@ -146,122 +146,122 @@ const handleGoBack = () => {
|
|||
}
|
||||
|
||||
const handleNextStep = (params = {}) => {
|
||||
addLog('进入 Step 3: 开始模拟')
|
||||
addLog('Entering Step 3: Start Simulation')
|
||||
|
||||
// 记录模拟轮数配置
|
||||
// Ghi lai cau hinh so vong simulation
|
||||
if (params.maxRounds) {
|
||||
addLog(`自定义模拟轮数: ${params.maxRounds} 轮`)
|
||||
addLog(`Custom simulation rounds: ${params.maxRounds} rounds`)
|
||||
} else {
|
||||
addLog('使用自动配置的模拟轮数')
|
||||
addLog('Using auto-configured simulation rounds')
|
||||
}
|
||||
|
||||
// 构建路由参数
|
||||
// Tao tham so route
|
||||
const routeParams = {
|
||||
name: 'SimulationRun',
|
||||
params: { simulationId: currentSimulationId.value }
|
||||
}
|
||||
|
||||
// 如果有自定义轮数,通过 query 参数传递
|
||||
// Neu co so vong tuy chinh, truyen qua query
|
||||
if (params.maxRounds) {
|
||||
routeParams.query = { maxRounds: params.maxRounds }
|
||||
}
|
||||
|
||||
// 跳转到 Step 3 页面
|
||||
// Dieu huong sang trang Step 3
|
||||
router.push(routeParams)
|
||||
}
|
||||
|
||||
// --- Data Logic ---
|
||||
|
||||
/**
|
||||
* 检查并关闭正在运行的模拟
|
||||
* 当用户从 Step 3 返回到 Step 2 时,默认用户要退出模拟
|
||||
* Kiem tra va dong simulation dang chay
|
||||
* Khi nguoi dung quay tu Step 3 ve Step 2, mac dinh la ho muon thoat simulation
|
||||
*/
|
||||
const checkAndStopRunningSimulation = async () => {
|
||||
if (!currentSimulationId.value) return
|
||||
|
||||
try {
|
||||
// 先检查模拟环境是否存活
|
||||
// Kiem tra truoc xem moi truong simulation con song khong
|
||||
const envStatusRes = await getEnvStatus({ simulation_id: currentSimulationId.value })
|
||||
|
||||
if (envStatusRes.success && envStatusRes.data?.env_alive) {
|
||||
addLog('检测到模拟环境正在运行,正在关闭...')
|
||||
addLog('Detected running simulation environment, closing...')
|
||||
|
||||
// 尝试优雅关闭模拟环境
|
||||
// Thu dong moi truong theo cach graceful
|
||||
try {
|
||||
const closeRes = await closeSimulationEnv({
|
||||
simulation_id: currentSimulationId.value,
|
||||
timeout: 10 // 10秒超时
|
||||
timeout: 10 // Timeout 10 giay
|
||||
})
|
||||
|
||||
if (closeRes.success) {
|
||||
addLog('✓ 模拟环境已关闭')
|
||||
addLog('✓ Simulation environment closed')
|
||||
} else {
|
||||
addLog(`关闭模拟环境失败: ${closeRes.error || '未知错误'}`)
|
||||
// 如果优雅关闭失败,尝试强制停止
|
||||
addLog(`Failed to close simulation environment: ${closeRes.error || 'Unknown error'}`)
|
||||
// Neu dong graceful that bai, thu force stop
|
||||
await forceStopSimulation()
|
||||
}
|
||||
} catch (closeErr) {
|
||||
addLog(`关闭模拟环境异常: ${closeErr.message}`)
|
||||
// 如果优雅关闭异常,尝试强制停止
|
||||
addLog(`Exception while closing simulation environment: ${closeErr.message}`)
|
||||
// Neu dong graceful gap exception, thu force stop
|
||||
await forceStopSimulation()
|
||||
}
|
||||
} else {
|
||||
// 环境未运行,但可能进程还在,检查模拟状态
|
||||
// Moi truong khong chay, nhung co the process van con, kiem tra trang thai simulation
|
||||
const simRes = await getSimulation(currentSimulationId.value)
|
||||
if (simRes.success && simRes.data?.status === 'running') {
|
||||
addLog('检测到模拟状态为运行中,正在停止...')
|
||||
addLog('Detected simulation status running, stopping...')
|
||||
await forceStopSimulation()
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// 检查环境状态失败不影响后续流程
|
||||
console.warn('检查模拟状态失败:', err)
|
||||
// Loi kiem tra trang thai moi truong khong anh huong luong xu ly tiep theo
|
||||
console.warn('Failed to check simulation status:', err)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制停止模拟
|
||||
* Force stop simulation
|
||||
*/
|
||||
const forceStopSimulation = async () => {
|
||||
try {
|
||||
const stopRes = await stopSimulation({ simulation_id: currentSimulationId.value })
|
||||
if (stopRes.success) {
|
||||
addLog('✓ 模拟已强制停止')
|
||||
addLog('✓ Simulation force-stopped')
|
||||
} else {
|
||||
addLog(`强制停止模拟失败: ${stopRes.error || '未知错误'}`)
|
||||
addLog(`Failed to force stop simulation: ${stopRes.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`强制停止模拟异常: ${err.message}`)
|
||||
addLog(`Force stop simulation exception: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const loadSimulationData = async () => {
|
||||
try {
|
||||
addLog(`加载模拟数据: ${currentSimulationId.value}`)
|
||||
addLog(`Loading simulation data: ${currentSimulationId.value}`)
|
||||
|
||||
// 获取 simulation 信息
|
||||
// Lay thong tin simulation
|
||||
const simRes = await getSimulation(currentSimulationId.value)
|
||||
if (simRes.success && simRes.data) {
|
||||
const simData = simRes.data
|
||||
|
||||
// 获取 project 信息
|
||||
// Lay thong tin 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(`Project loaded: ${projRes.data.project_id}`)
|
||||
|
||||
// 获取 graph 数据
|
||||
// Lay du lieu graph
|
||||
if (projRes.data.graph_id) {
|
||||
await loadGraph(projRes.data.graph_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addLog(`加载模拟数据失败: ${simRes.error || '未知错误'}`)
|
||||
addLog(`Failed to load simulation data: ${simRes.error || 'Unknown error'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`加载异常: ${err.message}`)
|
||||
addLog(`Load exception: ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -271,10 +271,10 @@ const loadGraph = async (graphId) => {
|
|||
const res = await getGraphData(graphId)
|
||||
if (res.success) {
|
||||
graphData.value = res.data
|
||||
addLog('图谱数据加载成功')
|
||||
addLog('Graph data loaded')
|
||||
}
|
||||
} catch (err) {
|
||||
addLog(`图谱加载失败: ${err.message}`)
|
||||
addLog(`Graph load failed: ${err.message}`)
|
||||
} finally {
|
||||
graphLoading.value = false
|
||||
}
|
||||
|
|
@ -287,12 +287,12 @@ const refreshGraph = () => {
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
addLog('SimulationView 初始化')
|
||||
addLog('SimulationView initialized')
|
||||
|
||||
// 检查并关闭正在运行的模拟(用户从 Step 3 返回时)
|
||||
// Kiem tra va dong simulation dang chay (khi nguoi dung quay ve tu Step 3)
|
||||
await checkAndStopRunningSimulation()
|
||||
|
||||
// 加载模拟数据
|
||||
// Tai du lieu simulation
|
||||
loadSimulationData()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue