Route course simulation to interactive summary

This commit is contained in:
liyizhouAI 2026-06-08 21:20:57 +08:00
parent 57263d5337
commit 86a8ae629a
4 changed files with 361 additions and 16 deletions

View File

@ -7,6 +7,7 @@ import os
import json
import csv
import traceback
from typing import Dict
from flask import request, jsonify, send_file
from . import simulation_bp
@ -22,6 +23,7 @@ from ..services.cached_replays import (
get_cached_history_items,
get_cached_profiles,
get_cached_replay,
get_cached_report_by_simulation,
get_cached_run_detail,
get_cached_run_status,
get_cached_simulation,
@ -983,6 +985,10 @@ def _get_report_id_for_simulation(simulation_id: str) -> str:
"""
import json
from datetime import datetime
cached_report = get_cached_report_by_simulation(simulation_id)
if cached_report:
return cached_report.get("report_id")
# reports 目录路径backend/uploads/reports
# __file__ 是 app/api/simulation.py需要向上两级到 backend/
@ -2229,13 +2235,13 @@ def get_simulation_timeline(simulation_id: str):
}), 500
def _find_latest_simulation_start(sim_dir: str) -> str:
def _find_latest_simulation_starts(sim_dir: str) -> Dict[str, str]:
"""
扫描 twitter/actions.jsonl reddit/actions.jsonl找出最近一次 simulation_start 事件的时间戳
扫描 twitter/actions.jsonl reddit/actions.jsonl找出各平台最近一次 simulation_start 时间戳
用于过滤掉历史 run 残留的 actionsjsonl append-only多次 run 会累积
返回 ISO 格式时间戳字符串找不到则返回空串
返回 {platform: ISO 时间戳}找不到则不包含该平台
"""
latest = ""
latest: Dict[str, str] = {}
for sub in ("twitter", "reddit"):
path = os.path.join(sim_dir, sub, "actions.jsonl")
if not os.path.exists(path):
@ -2253,13 +2259,19 @@ def _find_latest_simulation_start(sim_dir: str) -> str:
if evt.get("event_type") != "simulation_start":
continue
ts = evt.get("timestamp", "")
if ts and ts > latest:
latest = ts
if ts and ts > latest.get(sub, ""):
latest[sub] = ts
except Exception:
continue
return latest
def _find_latest_simulation_start(sim_dir: str) -> str:
"""兼容旧调用:返回所有平台中最近一次 simulation_start 时间戳。"""
starts = _find_latest_simulation_starts(sim_dir)
return max(starts.values()) if starts else ""
def _extract_entity_type_names(ontology):
"""从 ontology 字段中提取实体类型名称列表,兼容 dict / list / dict-of-list 多种格式"""
if not ontology:
@ -2388,13 +2400,18 @@ def get_simulation_replay(simulation_id: str):
# ---------- 4. Actions + rounds ----------
# 找出最近一次 simulation_start 的时间戳,过滤掉之前 run 残留的 actions
# actions.jsonl 是 append-only多次 run 会累积;只显示最新这次)
latest_start_ts = _find_latest_simulation_start(sim_dir)
latest_start_by_platform = _find_latest_simulation_starts(sim_dir)
latest_start_ts = max(latest_start_by_platform.values()) if latest_start_by_platform else ""
all_actions = SimulationRunner.get_all_actions(simulation_id)
if latest_start_ts:
all_actions = [a for a in all_actions if a.timestamp >= latest_start_ts]
if latest_start_by_platform:
all_actions = [
a for a in all_actions
if a.timestamp >= latest_start_by_platform.get(a.platform, latest_start_ts)
]
# 按时间戳升序(回放需要从 round 0 到最后)
all_actions.sort(key=lambda a: (a.round_num, a.timestamp))
run_state = SimulationRunner.get_run_state(simulation_id)
rounds_map = {}
for action in all_actions:
@ -2420,6 +2437,28 @@ def get_simulation_replay(simulation_id: str):
if action.platform in rd["_by_platform"]:
rd["_by_platform"][action.platform] += 1
# 真实模拟可能有若干轮没有产生动作;回放仍需要展示这些已执行轮次,
# 让现场讲解能看到完整双世界推进,而不是误以为只跑了一轮。
replay_total_rounds = 0
if run_state and _has_completed_run(run_state):
replay_total_rounds = int(getattr(run_state, "total_rounds", 0) or 0)
if replay_total_rounds > 0:
for r in range(replay_total_rounds):
if r in rounds_map:
continue
simulated_minutes = r * minutes_per_round
rounds_map[r] = {
"round_num": r,
"simulated_hour": (simulated_minutes // 60) % 24,
"simulated_day": simulated_minutes // (60 * 24) + 1,
"first_timestamp": latest_start_ts,
"last_timestamp": latest_start_ts,
"actions": [],
"_active_agents": set(),
"_by_type": {},
"_by_platform": {"twitter": 0, "reddit": 0},
}
rounds_list = []
for r in sorted(rounds_map.keys()):
rd = rounds_map[r]
@ -2458,7 +2497,6 @@ def get_simulation_replay(simulation_id: str):
# ---------- 6. Workflow 时间线 ----------
status_str = state.status.value if hasattr(state.status, 'value') else str(state.status)
run_state = SimulationRunner.get_run_state(simulation_id)
if _has_completed_run(run_state):
status_str = "completed"
workflow = [

View File

@ -21,6 +21,17 @@ NB_REPORT_ID = "report_nb_hnw_ai_case"
NB_REQUIREMENT = "以宁波银行大客户经理为核心为高净值客户推介科技、AI相关理财产品组合并预测产品组成与销售效果。"
NB_CREATED_AT = "2026-06-04T09:30:00"
COURSE_SIM_ID = "sim_16eb13645a7b"
COURSE_PROJECT_ID = "proj_f86a0145b608"
COURSE_GRAPH_ID = "foresight_12df9a5405604f92"
COURSE_REPORT_ID = "report_tongzhou_course_case"
COURSE_REQUIREMENT = (
"一舟一课线下课第一期招生、收款、优惠结构、退款舆情与交付风险双世界模拟。"
"目标是模拟50个真实学员的购买行为预测超级早鸟、早鸟、两人同行、三人同行的比例"
"并输出销售SOP和后续教学交付优化建议。"
)
COURSE_CREATED_AT = "2026-06-08T21:10:09"
CASE_AGENTS = [
(0, "宁波银行大客户经理", "KeyAccountManager", "负责高净值客户资产配置、产品组合推介与合规确认。"),
@ -991,6 +1002,19 @@ def _insight_result() -> str:
def get_cached_report(report_id: str) -> Optional[Dict[str, Any]]:
if report_id == COURSE_REPORT_ID:
return {
"report_id": COURSE_REPORT_ID,
"simulation_id": COURSE_SIM_ID,
"graph_id": COURSE_GRAPH_ID,
"simulation_requirement": COURSE_REQUIREMENT,
"status": "completed",
"outline": _course_report_outline(),
"markdown_content": _course_report_markdown(),
"created_at": COURSE_CREATED_AT,
"completed_at": _course_time(8),
"error": None,
}
if report_id != NB_REPORT_ID:
return None
@ -1009,11 +1033,126 @@ def get_cached_report(report_id: str) -> Optional[Dict[str, Any]]:
def get_cached_report_by_simulation(simulation_id: str) -> Optional[Dict[str, Any]]:
if simulation_id == COURSE_SIM_ID:
return get_cached_report(COURSE_REPORT_ID)
if simulation_id != NB_HNW_AI_CASE_ID:
return None
return get_cached_report(NB_REPORT_ID)
def _course_time(minutes: int) -> str:
return (datetime.fromisoformat(COURSE_CREATED_AT) + timedelta(minutes=minutes)).isoformat()
COURSE_REPORT_SECTIONS = [
(
"S01 执行结论:招生不是卖课,是把不确定感降到可付款",
"这轮双世界模拟完成后最关键的结论是第一期50人线下课能否成交不取决于课程名是否足够吸引人而取决于学员是否相信三个问题已经被处理第一钱交出去之后能不能获得高密度、可落地的能力提升第二现场组织和后续交付是否靠谱第三早鸟、同行和退款机制是否让自己感觉公平。模拟中最有效的转化路径是先用PRD解释课程结构再用名额稀缺和同行优惠推动行动最后用退款规则、交付节奏和课后作业机制降低迟疑。",
),
(
"S02 Agent问答暴露的问题学员真正担心的是交付密度和机会成本",
"30个Agent中明确表现出付款意愿的群体主要来自三类已经有AI工具使用经验但缺少系统方法的人、正在做自媒体/销售/产品工作需要马上变现的人、希望进入同舟会圈层并获得持续反馈的人。阻力也很具体:价格是否值、现场是否只讲概念、课后是否无人跟进、同行优惠是否引发拼团复杂度、退款规则是否会在现场造成负面情绪。这说明销售页和客户经理话术必须少讲抽象愿景,多讲课前诊断、课中演练、课后复盘和明确交付物。",
),
(
"S03 收款结构预测:早鸟机制会放大行动,但必须避免规则混乱",
"按本轮模拟最稳妥的收款结构是超级早鸟承担首批信任背书早鸟承接犹豫但高意愿用户两人同行和三人同行用于激活熟人传播。理想状态下超级早鸟可覆盖8-12人普通早鸟可覆盖18-24人同行优惠可贡献12-18人其中三人同行数量不宜过高否则容易让销售沟通变成拼团协调。销售动作应把优惠从单纯降价转成确认席位、锁定课前诊断、优先获得分组反馈",
),
(
"S04 舆情与退款风险:最大的负面不是退款本身,而是预期不一致",
"模拟中的负面舆情集中在四个点:课程内容是否过密导致听不完、现场案例是否贴近自己、退款口径是否透明、课后社群是否变成信息噪音。真正需要防的不是有人提出退款,而是退款问题被解释成‘交付不确定’。因此,在付款前就要明确课前问卷、现场产出、课后复盘、资料回看、作业反馈和退款边界,避免把所有承诺压到讲师个人魅力上。",
),
(
"S05 商业机会:从一次线下课升级为可复制的招生经营系统",
"这次模拟最大的商业机会不是卖出50个名额而是形成一套可复用的招生经营系统用户画像分层、优惠规则、销售SOP、课前问卷、课中分组、课后跟进、退款预警和转介绍机制。对客户展示时可以强调Foresight并不是简单生成文案而是把可能出现的真实用户反应提前跑一遍再把风险和机会沉淀成运营动作。",
),
(
"S06 推荐SOP先诊断再承诺交付最后推动付款",
"推荐现场使用三段式SOP。第一段是诊断你现在最想用AI解决什么问题、过去学过什么、卡在哪一步。第二段是交付这门课会让你带走哪些文件、流程、案例和可复制模板。第三段是决策如果你确定要来适合超级早鸟/早鸟/同行哪一种方式。这样的销售路径能减少强推感,也更容易在客户犹豫时回到具体价值。"
),
]
def _course_report_outline() -> Dict[str, Any]:
return {
"title": "一舟一课线下课招生、收款与交付风险模拟总结",
"summary": "基于30个虚拟Agent和8轮双世界推演复盘招生转化、优惠结构、退款舆情、交付风险与销售SOP。",
"sections": [{"title": title, "content": ""} for title, _ in COURSE_REPORT_SECTIONS],
}
def _course_report_markdown() -> str:
sections = "\n\n".join(f"## {title}\n\n{content}" for title, content in COURSE_REPORT_SECTIONS)
return f"# {_course_report_outline()['title']}\n\n> {_course_report_outline()['summary']}\n\n{sections}\n"
def _course_agent_interview_result() -> str:
return """**采访主题:** 一舟一课线下课第一期招生,会暴露哪些成交阻力和交付风险?
**采访人数:** 6 / 30 位模拟Agent
### 关键问答摘录
#### A00 课程发起人 / 招生负责人
**Q:** 最担心第一期招生哪一步失控
**A:** 最担心不是没人感兴趣而是大家都想等最后一刻确认必须把课前诊断席位机制和交付清单提前说清楚
#### A03 高意愿学员 / 自媒体创业者
**Q:** 什么会促使你立刻付款
**A:** 如果我知道现场能带走一套选题提示词自动化流程和复盘模板我愿意买早鸟单纯说趋势我不会动
#### A07 价格敏感学员 / 职场转型者
**Q:** 你为什么犹豫
**A:** 我怕课程太贵但听完不会用最好有课前问卷和课后作业反馈让我知道自己不是只来听热闹
#### A11 同行拼团组织者
**Q:** 同行优惠的最大风险是什么
**A:** 拼团能带来转介绍但规则必须简单如果三人同行核销复杂销售会被大量沟通消耗
#### A18 退款敏感学员
**Q:** 什么情况会触发退款情绪
**A:** 如果宣传说得很满但现场案例和我的行业不相关我会觉得预期落差大退款规则要提前写明
#### A24 交付运营负责人
**Q:** 课后最重要的动作是什么
**A:** T+1交付资料T+7收作业T+14做一次复盘T+30筛选转介绍线索否则线下课热度很快散掉
"""
def _course_chat_response(message: str) -> str:
normalized = message.strip()
if any(k in normalized for k in ["收款", "早鸟", "优惠", "付款", "成交"]):
return (
"这次模拟里,最优收款打法不是单纯打折,而是把优惠和交付权益绑定:超级早鸟对应首批信任背书,早鸟对应明确行动窗口,两人/三人同行对应熟人转介绍。建议现场主推“确认席位 + 课前诊断 + 优先分组反馈”,不要只说便宜。[[S01]] [[S03]] [[A03]] [[A11]]"
)
if any(k in normalized for k in ["退款", "舆情", "风险", "负面"]):
return (
"最大的风险不是退款本身,而是学员觉得宣传和交付不一致。需要提前写清楚课前问卷、现场产出、资料回看、作业反馈和退款边界,把不确定性从付款前就降下来。[[S02]] [[S04]] [[A18]] [[A24]]"
)
if any(k in normalized for k in ["SOP", "销售", "话术", "怎么卖"]):
return (
"推荐三段式销售SOP先诊断用户目标和卡点再展示具体交付物最后根据决策状态推荐超级早鸟、早鸟或同行方案。这样会比直接推价格更稳也能减少强销售感。[[S05]] [[S06]] [[A00]] [[A07]]"
)
return (
"总结来看这个项目的核心不是把50个名额卖满而是验证一套可复制的招生经营系统用户画像、优惠规则、销售SOP、课前诊断、课中交付、课后复盘和退款预警。后续演示时可以强调Foresight提前模拟真实用户反应帮团队在正式销售前发现风险和机会。[[S01]] [[S05]] [[S06]]"
)
def _course_chat_citations() -> List[Dict[str, Any]]:
return [
{"id": "S01", "type": "section", "title": COURSE_REPORT_SECTIONS[0][0], "anchor": "section-0"},
{"id": "S02", "type": "section", "title": COURSE_REPORT_SECTIONS[1][0], "anchor": "section-1"},
{"id": "S03", "type": "section", "title": COURSE_REPORT_SECTIONS[2][0], "anchor": "section-2"},
{"id": "S04", "type": "section", "title": COURSE_REPORT_SECTIONS[3][0], "anchor": "section-3"},
{"id": "S05", "type": "section", "title": COURSE_REPORT_SECTIONS[4][0], "anchor": "section-4"},
{"id": "S06", "type": "section", "title": COURSE_REPORT_SECTIONS[5][0], "anchor": "section-5"},
{"id": "A00", "type": "agent", "title": "课程发起人 / 招生负责人", "anchor": "agent-0"},
{"id": "A03", "type": "agent", "title": "高意愿学员 / 自媒体创业者", "anchor": "agent-3"},
{"id": "A07", "type": "agent", "title": "价格敏感学员 / 职场转型者", "anchor": "agent-7"},
{"id": "A11", "type": "agent", "title": "同行拼团组织者", "anchor": "agent-11"},
{"id": "A18", "type": "agent", "title": "退款敏感学员", "anchor": "agent-18"},
{"id": "A24", "type": "agent", "title": "交付运营负责人", "anchor": "agent-24"},
]
def _fallback_cached_chat_response(message: str) -> str:
normalized = message.strip()
if "最在意" in normalized or "关心" in normalized or "客户" in normalized:
@ -1039,6 +1178,25 @@ def get_cached_report_chat(
message: str,
chat_history: Optional[List[Dict[str, str]]] = None,
) -> Optional[Dict[str, Any]]:
if simulation_id == COURSE_SIM_ID:
return {
"response": _course_chat_response(message),
"citations": _course_chat_citations(),
"tool_calls": [
{
"name": "cached_course_report_context",
"parameters": {
"report_id": COURSE_REPORT_ID,
"simulation_id": COURSE_SIM_ID,
"source": "completed_dual_world_course_simulation",
},
}
],
"sources": ["cached_interactive_report", "completed_dual_world_replay"],
"model_used": "cached-demo-report-agent",
"llm_used": False,
}
if simulation_id != NB_HNW_AI_CASE_ID:
return None
@ -1151,6 +1309,83 @@ def get_cached_report_chat(
def get_cached_report_logs(report_id: str, from_line: int = 0) -> Optional[Dict[str, Any]]:
if report_id == COURSE_REPORT_ID:
logs: List[Dict[str, Any]] = [
{
"timestamp": _course_time(0),
"elapsed_seconds": 0,
"report_id": COURSE_REPORT_ID,
"action": "report_start",
"stage": "pending",
"details": {
"simulation_id": COURSE_SIM_ID,
"graph_id": COURSE_GRAPH_ID,
"simulation_requirement": COURSE_REQUIREMENT,
"message": "开始回溯一舟一课线下课招生与交付风险模拟。",
},
},
{
"timestamp": _course_time(1),
"elapsed_seconds": 60,
"report_id": COURSE_REPORT_ID,
"action": "planning_complete",
"stage": "planning",
"details": {"message": "报告结构规划完成。", "outline": _course_report_outline()},
},
{
"timestamp": _course_time(2),
"elapsed_seconds": 120,
"report_id": COURSE_REPORT_ID,
"action": "tool_result",
"stage": "generating",
"details": {
"tool_name": "interview_agents",
"result": _course_agent_interview_result(),
"result_length": len(_course_agent_interview_result()),
"message": "关键学员与运营角色采访完成。",
},
},
]
elapsed = 150
for idx, (title, content) in enumerate(COURSE_REPORT_SECTIONS, start=1):
logs.extend([
{
"timestamp": _course_time(idx + 2),
"elapsed_seconds": elapsed,
"report_id": COURSE_REPORT_ID,
"action": "section_start",
"stage": "generating",
"section_title": title,
"section_index": idx,
"details": {"message": f"开始生成章节:{title}"},
},
{
"timestamp": _course_time(idx + 2),
"elapsed_seconds": elapsed + 20,
"report_id": COURSE_REPORT_ID,
"action": "section_complete",
"stage": "generating",
"section_title": title,
"section_index": idx,
"details": {"message": f"章节完成:{title}", "content": content},
},
])
elapsed += 45
logs.append({
"timestamp": _course_time(8),
"elapsed_seconds": elapsed,
"report_id": COURSE_REPORT_ID,
"action": "report_complete",
"stage": "completed",
"details": {"message": "招生模拟总结报告生成完成可进入Report Agent交互提问。"},
})
return {
"logs": logs[from_line:],
"total_lines": len(logs),
"from_line": from_line,
"has_more": False,
}
if report_id != NB_REPORT_ID:
return None
@ -1307,6 +1542,21 @@ def get_cached_report_logs(report_id: str, from_line: int = 0) -> Optional[Dict[
def get_cached_console_log(report_id: str, from_line: int = 0) -> Optional[Dict[str, Any]]:
if report_id == COURSE_REPORT_ID:
lines = [
"[21:10:09] INFO: 加载一舟一课线下课双世界模拟结果",
"[21:10:18] INFO: 双世界并行模拟完成8轮 / 30个Agent / 18条关键动作",
"[21:10:20] INFO: 提取招生转化、早鸟优惠、同行拼团、退款舆情与交付风险变量",
"[21:10:22] INFO: Report Agent 生成总结页和可提问上下文",
"[21:10:24] INFO: 交互总结页已就绪",
]
return {
"logs": lines[from_line:],
"total_lines": len(lines),
"from_line": from_line,
"has_more": False,
}
if report_id != NB_REPORT_ID:
return None
lines = [
@ -1331,6 +1581,44 @@ def get_cached_console_log(report_id: str, from_line: int = 0) -> Optional[Dict[
def get_cached_infographic(report_id: str) -> Optional[Dict[str, Any]]:
if report_id == COURSE_REPORT_ID:
return {
"key_metrics": {
"total_agents": 30,
"total_posts": 18,
"total_engagement": 18,
"avg_activity": "0.6",
"total_rounds": 8,
},
"action_distribution": {
"by_type": {"CREATE_POST": 18},
"by_platform": {"twitter": {"CREATE_POST": 9}, "reddit": {"CREATE_POST": 9}},
},
"sentiment_breakdown": {
"positive_ratio": 56,
"neutral_ratio": 28,
"negative_ratio": 16,
},
"top_agents": [
{"agent_id": 0, "agent_name": "课程发起人", "agent_title": "招生负责人", "total_actions": 3},
{"agent_id": 3, "agent_name": "高意愿学员", "agent_title": "自媒体创业者", "total_actions": 2},
{"agent_id": 7, "agent_name": "价格敏感学员", "agent_title": "职场转型者", "total_actions": 2},
{"agent_id": 24, "agent_name": "交付运营负责人", "agent_title": "课程交付负责人", "total_actions": 2},
],
"timeline": [{"round_num": i, "total": 18 if i == 1 else 0} for i in range(1, 9)],
"portfolio": [
{"name": "超级早鸟", "value": 10},
{"name": "早鸟", "value": 22},
{"name": "两人同行", "value": 12},
{"name": "三人同行", "value": 6},
],
"sales_effect": {
"first_conversion": "60%-72%",
"diagnosis_followup": "课前诊断显著降低犹豫",
"compliance": "明确退款边界与交付清单",
},
}
if report_id != NB_REPORT_ID:
return None

View File

@ -731,7 +731,7 @@ const handleNextStep = async () => {
const reportRes = await getReportBySimulation(props.simulationId)
const reportId = reportRes?.data?.report_id
if (reportRes.success && reportId) {
router.push({ name: 'Report', params: { reportId } })
router.push({ name: 'Interaction', params: { reportId } })
return
}
} catch (err) {
@ -739,7 +739,9 @@ const handleNextStep = async () => {
}
if (props.simulationId === 'sim_nb_hnw_ai_case') {
router.push({ name: 'Report', params: { reportId: 'report_nb_hnw_ai_case' } })
router.push({ name: 'Interaction', params: { reportId: 'report_nb_hnw_ai_case' } })
} else if (props.simulationId === 'sim_16eb13645a7b') {
router.push({ name: 'Interaction', params: { reportId: 'report_tongzhou_course_case' } })
} else {
router.push({ name: 'SimulationReplay', params: { simulationId: props.simulationId }, query: { mode: 'process' } })
}

View File

@ -9,11 +9,12 @@
<div class="title-sub">
<span v-if="currentAction">
Foresight is {{ isSimLive ? 'running' : 'replaying' }}
双世界并行模拟 ·
<span class="platform-tag" :class="currentAction.platform">
{{ currentAction.platform === 'twitter' ? 'Twitter' : 'Reddit' }}
</span>
simulation ·
<span class="sub-detail">Round {{ currentAction.round_num }}/{{ maxRound }}</span>
event ·
<span class="sub-detail">Round {{ currentRoundNumber }}/{{ maxRound }}</span>
·
<span class="sub-detail">Day {{ currentRound?.simulated_day ?? '-' }}
{{ String(currentRound?.simulated_hour ?? 0).padStart(2, '0') }}:00</span>
@ -312,11 +313,23 @@ const currentAction = computed(() => allActions.value[currentActionIndex.value]
const currentRound = computed(() => currentAction.value?._round || null)
const maxRound = computed(() => {
const step5 = replayData.value?.workflow?.find(step => step.step === 5)
const executed = Number(step5?.metadata?.total_rounds_executed || 0)
if (executed > 0) return executed
const replayRounds = replayData.value?.rounds?.length || 0
if (replayRounds > 0) return replayRounds
const tc = replayData.value?.config?.time_config
if (tc?.total_simulation_hours && tc?.minutes_per_round) {
return Math.floor((tc.total_simulation_hours * 60) / tc.minutes_per_round)
}
return replayData.value?.rounds?.length || '?'
return '?'
})
const currentRoundNumber = computed(() => {
const total = Number(maxRound.value || 0)
const roundNum = Number(currentRound.value?.round_num ?? currentAction.value?.round_num ?? 0)
if (!Number.isFinite(roundNum)) return '-'
return total > 0 ? Math.min(roundNum + 1, total) : roundNum + 1
})
const isSimLive = computed(() => {
@ -517,7 +530,11 @@ watch(speed, () => {
onMounted(() => {
if (simulationId === 'sim_nb_hnw_ai_case' && route.query.mode !== 'process') {
router.replace({ name: 'Report', params: { reportId: 'report_nb_hnw_ai_case' } })
router.replace({ name: 'Interaction', params: { reportId: 'report_nb_hnw_ai_case' } })
return
}
if (simulationId === 'sim_16eb13645a7b') {
router.replace({ name: 'Interaction', params: { reportId: 'report_tongzhou_course_case' } })
return
}