translate frontend folder

This commit is contained in:
TQuynh109 2026-03-26 04:47:45 +00:00
parent e28c5296f5
commit e09cfe9985
22 changed files with 1671 additions and 1648 deletions

View File

@ -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

View File

@ -3,11 +3,11 @@
</template>
<script setup>
// 使 Vue Router
// S dng Vue Router đ qun 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;
}

View File

@ -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 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) {

View File

@ -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 {

View File

@ -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) => {

View File

@ -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 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 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 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 } })
}

View File

@ -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) // Mc đnh hin th nhãn cnh
const expandedSelfLoops = ref(new Set()) // Các self-loop đang m
const showSimulationFinishedHint = ref(false) // Thông báo sau khi mô phng kết thúc
const wasSimulating = ref(false) // Theo dõi trưc đó có đang mô phng hay không
//
// Đóng thông báo kết thúc mô phng
const dismissFinishedHint = () => {
showSimulationFinishedHint.value = false
}
// isSimulating
// Theo dõi thay đi isSimulating đ phát hin kết thúc mô phng
watch(() => props.isSimulating, (newValue, oldValue) => {
if (wasSimulating.value && !newValue) {
//
// T trng thái mô phng chuyn sang không mô phng, hin th thông báo kết thúc
showSimulationFinishedHint.value = true
}
wasSimulating.value = newValue
}, { immediate: true })
// /
// Toggle trng thái m/đóng ca 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 loi entity đ hin th legend
const entityTypes = computed(() => {
if (!props.graphData?.nodes) return []
const typeMap = {}
//
// Bng 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 thi 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 trng thái m
}
let currentSimulation = null
@ -328,7 +328,7 @@ let linkLabelBgRef = null
const renderGraph = () => {
if (!graphSvg.value || !props.graphData) return
// 仿
// Dng simulation trưc đó
if (currentSimulation) {
currentSimulation.stop()
}
@ -349,7 +349,7 @@ const renderGraph = () => {
if (nodesData.length === 0) return
// Prep data
// Chun b d liu
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 liu cnh, tính s lưng và ch s cnh gia cùng mt cp 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))
//
// Thng kê s cnh gia mi cp node, thu thp self-loop
tempEdges.forEach(e => {
if (e.source_node_uuid === e.target_node_uuid) {
// -
// Self-loop - gom vào mng
if (!selfLoopEdges[e.source_node_uuid]) {
selfLoopEdges[e.source_node_uuid] = []
}
@ -386,9 +386,9 @@ const renderGraph = () => {
}
})
//
// Ghi nhn đang x lý cnh th my ca mi cp 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 - mi node ch thêm mt self-loop tng hp
if (processedSelfLoopNodes.has(e.source_node_uuid)) {
return //
return // Đã x lý ri, 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 ca self-loop
}
})
return
@ -428,19 +428,19 @@ const renderGraph = () => {
const currentIndex = edgePairIndex[pairKey] || 0
edgePairIndex[pairKey] = currentIndex + 1
// UUID < UUID
// Kim tra hưng cnh có trùng vi hưng chun hay không (source UUID < target UUID)
const isReversed = e.source_node_uuid > e.target_node_uuid
// 线
// Tính đ cong: nhiu cnh thì tách ra, mt cnh thì là đưng thng
let curvature = 0
if (totalCount > 1) {
//
//
// Phân b đ cong đng đu, đm bo d phân bit
// Phm vi đ cong tăng theo s lưng cnh, càng nhiu cnh thì phm vi càng ln
const curvatureRange = Math.min(1.2, 0.6 + totalCount * 0.15)
curvature = ((currentIndex / (totalCount - 1)) - 0.5) * curvatureRange * 2
//
//
// Nếu hưng cnh ngưc vi hưng chun, đo đ cong
// Nh vy tt c cnh đưc phân b trong cùng h quy chiếu, tránh chng ln 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 - điu chnh khong cách node theo s lưng cnh
const simulation = d3.forceSimulation(nodes)
.force('link', d3.forceLink(edges).id(d => d.id).distance(d => {
//
// 150 40
// Điu chnh khong cách da trên s cnh gia cp node này
// Khong cách cơ bn là 150, mi cnh 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 lc hút v trung tâm đ các cm node đc lp gom li gn 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 dn cho đưng cong
const getLinkPath = (d) => {
const sx = d.source.x, sy = d.source.y
const tx = d.target.x, ty = d.target.y
//
// Kim tra self-loop
if (d.isSelfLoop) {
//
// Self-loop: v mt cung tròn đi ra t node ri quay li
const loopRadius = 30
//
const x1 = sx + 8 //
// Bt đu t bên phi node, đi mt vòng ri quay li
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 chiu kim đng h)
return `M${x1},${y1} A${loopRadius},${loopRadius} 0 1,1 ${x2},${y2}`
}
if (d.curvature === 0) {
// 线
// Đưng thng
return `M${sx},${sy} L${tx},${ty}`
}
// 线 -
// Tính đim điu khin ca đưng cong - điu chnh đng theo s cnh và khong cách
const dx = tx - sx, dy = ty - sy
const dist = Math.sqrt(dx * dx + dy * dy)
// 线线
//
// Đ lch vuông góc vi hưng ni, tính theo t l khong cách đ đm bo nhìn rõ
// Càng nhiu cnh, t l đ lch theo khong cách càng ln
const pairTotal = d.pairTotal || 1
const offsetRatio = 0.25 + pairTotal * 0.05 // 25%5%
const offsetRatio = 0.25 + pairTotal * 0.05 // Giá tr cơ bn là 25%, và nó tăng thêm 5% cho mi cnh 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 đim ca đư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
//
// Kim tra self-loop
if (d.isSelfLoop) {
//
// V trí label self-loop: bên phi 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 đim ca đưng Bezier bc hai ti 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 thc Bezier bc hai B(t) = (1-t)²P0 + 2(1-t)tP1 + t²P2, vi 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 cnh đã chn 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 cnh đang chn
d3.select(event.target).attr('stroke', '#3498db').attr('stroke-width', 3)
selectedItem.value = {
@ -590,7 +590,7 @@ const renderGraph = () => {
}
})
// Link labels background (使)
// Nn label ca cnh (nn trng 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 cnh 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 cnh 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 đ điu khin hin 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 nhn v trí, không restart simulation (phân bit 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) => {
//
// Kim tra có thc s bt đu drag hay chưa (dch chuyn 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 tht s drag mi restart simulation
d._isDragging = true
simulation.alphaTarget(0.3).restart()
}
@ -686,7 +686,7 @@ const renderGraph = () => {
}
})
.on('end', (event, d) => {
// 仿
// Ch khi thc s drag mi cho simulation gim dn
if (d._isDragging) {
simulation.alphaTarget(0)
}
@ -697,12 +697,12 @@ const renderGraph = () => {
)
.on('click', (event, d) => {
event.stopPropagation()
//
// Reset style ca tt 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 chn
d3.select(event.target).attr('stroke', '#E91E63').attr('stroke-width', 4)
//
// Highlight các cnh ni vi 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', () => {
// 线
// Cp nht đưng cong
link.attr('d', d => getLinkPath(d))
//
// Cp nht v trí nhãn cnh (không xoay, hin 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', '') // Loi b chuyn đng xoay, gi nguyên v trí nm ngang.
})
//
// Cp nht nn nhãn cnh
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', '') // Loi b phép xoay
})
node
@ -773,7 +773,7 @@ const renderGraph = () => {
.attr('y', d => d.y)
})
//
// Click vào vùng trng đ đó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 tc hin th nhãn cnh
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);

View File

@ -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()
//
// Trng 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 hin đưc chn (dùng cho modal)
let observer = null
let isAnimating = false //
let expandDebounceTimer = null //
let pendingState = null //
let isAnimating = false // Khóa animation, tránh nhp nháy
let expandDebounceTimer = null // B hn gi debounce
let pendingState = null // Ghi li trng thái đích ch thc thi
// -
// Cu hình layout th - điu chnh sang t l rng hơn
const CARDS_PER_ROW = 4
const CARD_WIDTH = 280
const CARD_HEIGHT = 280
const CARD_GAP = 24
//
// Tính đng style chiu cao container
const containerStyle = computed(() => {
if (!isExpanded.value) {
//
// Trng thái thu gn: chiu cao c đnh
return { minHeight: '420px' }
}
//
// Trng thái m rng: tính chiu 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 chiu cao thc tế cn dùng: s hàng * chiu cao th + (s hàng-1) * khong cách + mt ít khong đm dưi
const expandedHeight = rows * CARD_HEIGHT + (rows - 1) * CARD_GAP + 10
return { minHeight: `${expandedHeight}px` }
})
//
// Ly style ca th
const getCardStyle = (index) => {
const total = projects.value.length
if (isExpanded.value) {
//
// Trng thái m rng: layout dng 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 hin ti đ căn gia tng 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 rng xung dưi, tăng khong cách vi tiêu đ
const y = 20 + row * (CARD_HEIGHT + CARD_GAP)
return {
@ -267,14 +267,14 @@ const getCardStyle = (index) => {
transition: transition
}
} else {
//
// Trng thái thu gn: xếp chng hình qut
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
//
// Điu chnh v trí bt đu, gn tiêu đ nhưng vn gi khong cách phù hp
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) => {
}
}
//
// Ly 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 bt đu
return 'not-started'
} else if (current >= total) {
//
// Đã hoàn thành
return 'completed'
} else {
//
// Đang thc hin
return 'in-progress'
}
}
//
// Format ngày tháng (ch hin th phn ngày)
const formatDate = (dateStr) => {
if (!dateStr) return ''
try {
@ -316,7 +316,7 @@ const formatDate = (dateStr) => {
}
}
// :
// Format thi gian (hin th gi:phút)
const formatTime = (dateStr) => {
if (!dateStr) return ''
try {
@ -329,35 +329,35 @@ const formatTime = (dateStr) => {
}
}
//
// Ct ngn văn bn
const truncateText = (text, maxLength) => {
if (!text) return ''
return text.length > maxLength ? text.slice(0, maxLength) + '...' : text
}
// 20
// To tiêu đ t yêu cu mô phng (ly 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 đ hin th (ly 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 hin th s vòng (vòng hin ti/tng 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`
}
//
// Ly loi 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'
}
//
// Ly text nhãn loi file
const getFileTypeLabel = (filename) => {
if (!filename) return 'FILE'
const ext = filename.split('.').pop()?.toUpperCase()
return ext || 'FILE'
}
//
// Ct ngn tên file (gi li phn m rng)
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
// Điu hưng đến trang xây dng graph (Project)
const goToProject = () => {
if (selectedProject.value?.project_id) {
router.push({
@ -412,7 +412,7 @@ const goToProject = () => {
}
}
// Simulation
// Điu hưng đến trang cu hình môi trưng (Simulation)
const goToSimulation = () => {
if (selectedProject.value?.simulation_id) {
router.push({
@ -423,7 +423,7 @@ const goToSimulation = () => {
}
}
// Report
// Điu 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 = () => {
}
}
//
// Ti lch 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
// Khi to IntersectionObserver
const initObserver = () => {
if (observer) {
observer.disconnect()
@ -461,47 +461,47 @@ const initObserver = () => {
entries.forEach((entry) => {
const shouldExpand = entry.isIntersecting
//
// Cp nht trng thái đích ch thc thi (dù đang animation vn phi ghi li trng thái mi nht)
pendingState = shouldExpand
//
// Xóa b hn gi debounce trưc đó (ý đnh cun mi s ghi đè ý đnh cũ)
if (expandDebounceTimer) {
clearTimeout(expandDebounceTimer)
expandDebounceTimer = null
}
//
// Nếu đang animation thì ch ghi trng thái, ch x lý sau khi animation kết thúc
if (isAnimating) return
//
// Nếu trng thái đích trùng vi trng thái hin ti thì không cn x lý
if (shouldExpand === isExpanded.value) {
pendingState = null
return
}
// 使
// (50ms)(200ms)
// Dùng debounce đ trì hoãn chuyn trng thái, tránh nhp nháy nhanh
// Khi m rng thì delay ngn hơn (50ms), khi thu gn delay dài hơn (200ms) đ n đnh hơn
const delay = shouldExpand ? 50 : 200
expandDebounceTimer = setTimeout(() => {
//
// Kim tra có đang animation không
if (isAnimating) return
//
// Kim tra trng thái ch có còn cn thc thi không (có th đã b ln cun sau ghi đè)
if (pendingState === null || pendingState === isExpanded.value) return
//
// Bt khóa animation
isAnimating = true
isExpanded.value = pendingState
pendingState = null
//
// Sau khi animation hoàn tt thì m khóa, đng thi kim tra xem có trng thái mi đang ch hay không
setTimeout(() => {
isAnimating = false
//
// Sau khi animation kết thúc, kim tra xem có trng thái ch mi không
if (pendingState !== null && pendingState !== isExpanded.value) {
//
// Delay thêm mt chút trưc khi thc thi đ tránh chuyn quá nhanh
expandDebounceTimer = setTimeout(() => {
if (pendingState !== null && pendingState !== isExpanded.value) {
isAnimating = true
@ -518,20 +518,20 @@ const initObserver = () => {
})
},
{
// 使使
// Dùng nhiu ngưng đ vic phát hin mưt hơn
threshold: [0.4, 0.6, 0.8],
// rootMargin
// Điu chnh rootMargin, co đáy viewport lên trên đ phi cun nhiu hơn mi kích hot m rng
rootMargin: '0px 0px -150px 0px'
}
)
//
// Bt đu quan sát
if (historyContainer.value) {
observer.observe(historyContainer.value)
}
}
//
// Theo dõi thay đi route, khi quay li trang ch thì ti li d liu
watch(() => route.path, (newPath) => {
if (newPath === '/') {
loadHistory()
@ -539,28 +539,28 @@ watch(() => route.path, (newPath) => {
})
onMounted(async () => {
// DOM
// Đm bo DOM đã render xong ri mi ti d liu
await nextTick()
await loadHistory()
// DOM
// Ch DOM render xong ri mi khi to observer
setTimeout(() => {
initObserver()
}, 100)
})
// 使 keep-alive
// Nếu dùng keep-alive, ti li d liu khi component đưc kích hot
onActivated(() => {
loadHistory()
})
onUnmounted(() => {
// Intersection Observer
// Dn dp Intersection Observer
if (observer) {
observer.disconnect()
observer = null
}
//
// Dn dp b hn 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;

View File

@ -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 - to simulation và chuyn 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
// Chuyn đến trang mô phng
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

View File

@ -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 content nhưng chưa được xử 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 đã 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 truyn t Step2
minutesPerRound: {
type: Number,
default: 30 // 30
default: 30 // mc đnh mi 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 bt đu, 1: đang chy, 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 (cng dn tăng dn)
const actionIds = ref(new Set()) // tp ID action đ kh trùng lp
const scrollContainer = ref(null)
// Computed
//
// Hin th action theo th t thi gian (mi nht cui, tc phía dưi)
const chronologicalActions = computed(() => {
return allActions.value
})
//
// Đếm action ca tng 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 thi gian mô phng đã trôi qua (tính theo round và phút mi 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
// Thi gian mô phng đã trôi qua ca Twitter
const twitterElapsedTime = computed(() => {
return formatElapsedTime(runStatus.value.twitter_current_round || 0)
})
// Reddit
// Thi gian mô phng đã trôi qua ca 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 trng thái (dùng đ khi đng li mô phng)
const resetAllState = () => {
phase.value = 0
runStatus.value = {}
@ -373,46 +373,46 @@ const resetAllState = () => {
startError.value = null
isStarting.value = false
isStopping.value = false
stopPolling() //
stopPolling() // dng các polling cũ nếu có
}
//
// Khi đng mô phng
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 ln mô phng 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 bt đu li t đu
enable_graph_memory_update: true // bt cp nht 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
}
}
//
// Dng mô phng
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 trng thái
let statusTimer = null
let detailTimer = null
@ -482,7 +482,7 @@ const stopPolling = () => {
}
}
//
// Theo dõi round trưc đó ca tng platform đ phát hin thay đi và ghi log
const prevTwitterRound = ref(0)
const prevRedditRound = ref(0)
@ -497,7 +497,7 @@ const fetchRunStatus = async () => {
runStatus.value = data
//
// Kim tra thay đi round ca tng 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
// Kim tra mô phng đã hoàn thành chưa (theo runner_status hoc trng thái hoàn thành ca platform)
const isCompleted = data.runner_status === 'completed' || data.runner_status === 'stopped'
// runner_status
// twitter_completed reddit_completed
// Kim tra thêm: nếu backend chưa kp cp nht runner_status nhưng platform đã báo xong
// Da 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)
}
}
//
// Kim tra tt c platform đang bt đã hoàn thành chưa
const checkPlatformsCompleted = (data) => {
// false
// Nếu không có d liu platform nào thì tr v false
if (!data) return false
//
// Kim tra trng thái hoàn thành ca tng platform
const twitterCompleted = data.twitter_completed === true
const redditCompleted = data.reddit_completed === true
//
// actions_count count > 0 running true
// Nếu có ít nht mt platform hoàn thành, kim tra xem tt c platform đang bt đã hoàn thành chưa
// Dùng actions_count đ suy ra platform có đưc bt không (count > 0 hoc tng 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 bt thì tr v false
if (!twitterEnabled && !redditEnabled) return false
//
// Kim tra tt c platform đang bt đã 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 đ ly đy đ danh sách action
const serverActions = res.data.all_actions || []
//
// Thêm tăng dn các action mi (kh trùng lp)
let newActionsAdded = 0
serverActions.forEach(action => {
// ID
// To 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 cun, đ ngưi dùng t do xem timeline
// Action mi 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}`)
//
// Chuyn 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 xung cui
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

View File

@ -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;

View File

@ -1,6 +1,7 @@
/**
* 临时存储待上传的文件和需求
* 用于首页点击启动引擎后立即跳转在Process页面再进行API调用
* Lưu tạm file cần upload 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'

View File

@ -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 = () => {
})
}
// - APIProcess
// 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;

View File

@ -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>

View File

@ -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 dng đ th, 2: Thiết lp môi trưng, 3: Bt đu mô phng, 4: To 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 chuyn t Step 2 sang Step 3, ghi li cu hình s vòng mô phng
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

View File

@ -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>

View File

@ -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 li Step 2, cn đóng mô phng đang chy
addLog('Preparing to return to Step 2, shutting down simulation...')
// Dng vic polling
stopGraphRefresh()
try {
//
// Trưc tiên, th đóng môi trưng mô phng mt 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 chy, kim tra xem có cn dng 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 chy mô phng, auto-refresh s không hin th loading toàn màn hình đ tránh b nhp nháy
// Ch hin th loading khi refresh th công hoc khi ti ln đ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()
})

View File

@ -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>