| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- '''
- 知识获取工作流可视化
- 1. 读取知识获取工作流详细过程数据文件(参考 .cache/9f510b2a8348/execution_record.json)
- 2. 文件路径可设置,每个文件表示一个输入信息执行过程,可能有多个。多个文件路径硬编码在代码中,用list表示
- 3. 将知识获取工作流用html页面展示出来
- 4. HTML 文件输出路径在当前目录下,文件名称为 workflow_visualization_datetime.html
- '''
- import json
- import os
- from datetime import datetime
- from pathlib import Path
- # 硬编码的文件路径列表(相对于项目根目录)
- DATA_FILE_PATHS = [
- "../.cache/dd8dd68ebf0c/execution_record.json",
- "../.cache/70a208f70a7e/execution_record.json",
- "../.cache/996e626b9c85/execution_record.json",
- # 可以在这里添加更多文件路径
- ]
- def load_data_files(file_paths):
- """读取并解析JSON文件"""
- data_list = []
- script_dir = Path(__file__).parent
- for file_path in file_paths:
- # 将相对路径转换为绝对路径
- abs_path = (script_dir / file_path).resolve()
- if not abs_path.exists():
- print(f"警告: 文件不存在: {abs_path}")
- continue
- try:
- with open(abs_path, 'r', encoding='utf-8') as f:
- data = json.load(f)
- data_list.append(data)
- except (json.JSONDecodeError, IOError) as e:
- print(f"错误: 读取文件失败 {abs_path}: {e}")
- return data_list
- def parse_workflow_data(data):
- """解析工作流数据,提取关键信息"""
- workflow = {
- 'input': {},
- 'steps': [],
- 'output': {}
- }
-
- # 提取输入信息
- if 'input' in data:
- workflow['input'] = {
- 'question': data['input'].get('question', ''),
- 'post_info': data['input'].get('post_info', ''),
- 'persona_info': data['input'].get('persona_info', '')
- }
-
- # 提取执行流程(新格式:execution 直接包含各步骤,不再有 modules.function_knowledge)
- if 'execution' in data:
- execution = data['execution']
-
- # 步骤1: 生成query
- if 'generate_query' in execution:
- generate_query = execution['generate_query']
- workflow['steps'].append({
- 'step': 'generate_query',
- 'name': '生成查询',
- 'query': generate_query.get('query', '') or generate_query.get('response', ''),
- 'prompt': generate_query.get('prompt', '')
- })
-
- # 步骤2: 选择工具
- if 'select_tool' in execution:
- select_tool = execution['select_tool']
- response = select_tool.get('response', {})
- workflow['steps'].append({
- 'step': 'select_tool',
- 'name': '选择工具',
- 'prompt': select_tool.get('prompt', ''),
- 'tool_name': response.get('工具名', '') if isinstance(response, dict) else '',
- 'tool_id': response.get('工具调用ID', '') if isinstance(response, dict) else '',
- 'tool_usage': response.get('使用方法', '') if isinstance(response, dict) else ''
- })
-
- # 判断是否选择了工具(如果 response 为空字典或没有工具信息,则没有选择到工具)
- has_tool = False
- if 'select_tool' in execution:
- select_tool = execution['select_tool']
- response = select_tool.get('response', {})
- if isinstance(response, dict) and response.get('工具名'):
- has_tool = True
-
- # 如果选择了工具,执行工具调用流程
- if has_tool:
- # 步骤3: 提取参数
- if 'extract_params' in execution:
- extract_params = execution['extract_params']
- workflow['steps'].append({
- 'step': 'extract_params',
- 'name': '提取参数',
- 'prompt': extract_params.get('prompt', ''),
- 'params': extract_params.get('params', {})
- })
-
- # 步骤4: 执行工具(新格式:tool_call 替代 execute_tool)
- if 'tool_call' in execution:
- tool_call = execution['tool_call']
- # 优先使用 result,如果没有则使用 response
- result = tool_call.get('result', '')
- if not result and tool_call.get('response'):
- # 如果 response 是字典,尝试提取其中的 result
- response = tool_call.get('response', {})
- if isinstance(response, dict):
- result = response.get('result', response)
- else:
- result = response
- workflow['steps'].append({
- 'step': 'execute_tool',
- 'name': '执行工具',
- 'response': result or tool_call.get('response', '')
- })
-
- # 如果没有选择到工具,进行知识搜索流程
- else:
- if 'knowledge_search' in execution:
- knowledge_search = execution['knowledge_search']
-
- # 步骤3: LLM搜索(大模型+search 渠道的搜索过程)
- if 'llm_search' in knowledge_search:
- llm_search = knowledge_search['llm_search']
- search_results = llm_search.get('search_results', [])
- workflow['steps'].append({
- 'step': 'llm_search',
- 'name': 'LLM搜索',
- 'search_results': search_results
- })
-
- # 步骤4: 多渠道搜索结果整合
- if 'multi_search_merge' in knowledge_search:
- multi_search_merge = knowledge_search['multi_search_merge']
- workflow['steps'].append({
- 'step': 'multi_search_merge',
- 'name': '多渠道搜索结果整合',
- 'prompt': multi_search_merge.get('prompt', ''),
- 'response': multi_search_merge.get('response', ''),
- 'sources_count': multi_search_merge.get('sources_count', 0),
- 'valid_sources_count': multi_search_merge.get('valid_sources_count', 0)
- })
-
- # 提取输出信息
- if 'output' in data:
- workflow['output'] = {
- 'result': data['output'].get('result', '')
- }
-
- return workflow
- def escape_html(text):
- """转义HTML特殊字符"""
- if not isinstance(text, str):
- text = str(text)
- return (text.replace('&', '&')
- .replace('<', '<')
- .replace('>', '>')
- .replace('"', '"')
- .replace("'", '''))
- def format_json_for_display(obj):
- """格式化JSON对象用于显示"""
- if isinstance(obj, dict):
- return json.dumps(obj, ensure_ascii=False, indent=2)
- elif isinstance(obj, str):
- try:
- parsed = json.loads(obj)
- return json.dumps(parsed, ensure_ascii=False, indent=2)
- except (json.JSONDecodeError, ValueError):
- return obj
- return str(obj)
- def generate_html(workflows):
- """生成HTML页面"""
- html = '''<!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>知识获取工作流可视化</title>
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: #333;
- line-height: 1.6;
- min-height: 100vh;
- }
-
- .container {
- max-width: 1400px;
- margin: 0 auto;
- padding: 30px 20px;
- }
-
- h1 {
- text-align: center;
- color: white;
- margin-bottom: 40px;
- font-size: 32px;
- font-weight: 600;
- text-shadow: 0 2px 10px rgba(0,0,0,0.2);
- letter-spacing: 1px;
- }
-
- .tabs {
- display: flex;
- background: white;
- border-radius: 12px 12px 0 0;
- box-shadow: 0 4px 20px rgba(0,0,0,0.15);
- overflow-x: auto;
- padding: 5px;
- }
-
- .tab {
- padding: 16px 28px;
- cursor: pointer;
- border: none;
- background: transparent;
- color: #666;
- font-size: 14px;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- white-space: nowrap;
- border-radius: 8px;
- margin: 0 4px;
- position: relative;
- font-weight: 500;
- }
-
- .tab:hover {
- background: #f0f0f0;
- color: #333;
- }
-
- .tab.active {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
- }
-
- .tab-content {
- display: none;
- background: white;
- padding: 40px;
- border-radius: 0 0 12px 12px;
- box-shadow: 0 4px 20px rgba(0,0,0,0.15);
- margin-bottom: 20px;
- animation: fadeIn 0.3s ease-in;
- }
-
- @keyframes fadeIn {
- from { opacity: 0; transform: translateY(10px); }
- to { opacity: 1; transform: translateY(0); }
- }
-
- .tab-content.active {
- display: block;
- }
-
- .input-section {
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
- padding: 28px;
- border-radius: 12px;
- margin-bottom: 35px;
- box-shadow: 0 4px 15px rgba(0,0,0,0.1);
- border: 1px solid rgba(255,255,255,0.5);
- }
-
- .input-section h3 {
- color: #2c3e50;
- margin-bottom: 20px;
- font-size: 20px;
- font-weight: 600;
- display: flex;
- align-items: center;
- gap: 10px;
- }
-
- .input-section h3::before {
- content: '📋';
- font-size: 24px;
- }
-
- .input-item {
- margin-bottom: 16px;
- padding: 12px;
- background: rgba(255,255,255,0.7);
- border-radius: 8px;
- transition: all 0.3s;
- }
-
- .input-item:hover {
- background: rgba(255,255,255,0.9);
- transform: translateX(5px);
- }
-
- .input-item strong {
- color: #495057;
- display: inline-block;
- width: 110px;
- font-weight: 600;
- }
-
- .input-item .placeholder {
- color: #999;
- font-style: italic;
- }
-
- .workflow {
- position: relative;
- }
-
- .workflow-step {
- background: white;
- border: 2px solid #e0e0e0;
- border-radius: 12px;
- margin-bottom: 25px;
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- overflow: hidden;
- box-shadow: 0 2px 8px rgba(0,0,0,0.08);
- }
-
- .workflow-step.active {
- border-color: #667eea;
- box-shadow: 0 8px 24px rgba(102, 126, 234, 0.25);
- transform: translateY(-2px);
- }
-
- .step-header {
- padding: 20px 24px;
- background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
- cursor: pointer;
- display: flex;
- justify-content: space-between;
- align-items: center;
- user-select: none;
- transition: all 0.3s;
- }
-
- .step-header:hover {
- background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
- }
-
- .workflow-step.active .step-header {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- }
-
- .workflow-step.active .step-name {
- color: white;
- }
-
- .workflow-step.active .step-toggle {
- color: white;
- }
-
- .step-title {
- display: flex;
- align-items: center;
- gap: 15px;
- }
-
- .step-number {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- border-radius: 50%;
- font-size: 16px;
- font-weight: bold;
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
- }
-
- .workflow-step.active .step-number {
- background: white;
- color: #667eea;
- box-shadow: 0 4px 12px rgba(255,255,255,0.3);
- }
-
- .step-name {
- font-size: 18px;
- font-weight: 600;
- color: #2c3e50;
- }
-
- .step-toggle {
- color: #6c757d;
- font-size: 20px;
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- }
-
- .step-toggle.expanded {
- transform: rotate(180deg);
- }
-
- .step-content {
- padding: 0 20px;
- max-height: 0;
- overflow: hidden;
- transition: max-height 0.3s ease-out, padding 0.3s;
- }
-
- .step-content.expanded {
- max-height: 5000px;
- padding: 20px;
- }
-
- .step-detail {
- margin-bottom: 20px;
- }
-
- .step-detail-label {
- font-weight: 600;
- color: #495057;
- margin-bottom: 10px;
- display: block;
- font-size: 14px;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- }
-
- .step-detail-content {
- background: #f8f9fa;
- padding: 16px;
- border-radius: 8px;
- border-left: 4px solid #667eea;
- font-size: 14px;
- line-height: 1.8;
- white-space: pre-wrap;
- word-wrap: break-word;
- max-height: 400px;
- overflow-y: auto;
- box-shadow: 0 2px 8px rgba(0,0,0,0.05);
- }
-
- .json-content {
- font-family: 'SF Mono', 'Monaco', 'Courier New', monospace;
- background: #1e1e1e;
- color: #d4d4d4;
- padding: 20px;
- border-radius: 8px;
- overflow-x: auto;
- border-left: 4px solid #667eea;
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- }
-
- .prompt-toggle-btn {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- border: none;
- padding: 10px 20px;
- border-radius: 6px;
- cursor: pointer;
- font-size: 13px;
- font-weight: 500;
- margin-top: 15px;
- transition: all 0.3s;
- box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
- }
-
- .prompt-toggle-btn:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
- }
-
- .prompt-content {
- display: none;
- margin-top: 15px;
- padding: 16px;
- background: #fff3cd;
- border-radius: 8px;
- border-left: 4px solid #ffc107;
- font-size: 13px;
- line-height: 1.8;
- white-space: pre-wrap;
- word-wrap: break-word;
- max-height: 500px;
- overflow-y: auto;
- }
-
- .prompt-content.show {
- display: block;
- animation: slideDown 0.3s ease-out;
- }
-
- @keyframes slideDown {
- from {
- opacity: 0;
- max-height: 0;
- }
- to {
- opacity: 1;
- max-height: 500px;
- }
- }
-
- .output-section {
- background: linear-gradient(135deg, #e0f2fe 0%, #bae6fd 100%);
- padding: 28px;
- border-radius: 12px;
- margin-top: 35px;
- border-left: 4px solid #0ea5e9;
- box-shadow: 0 4px 15px rgba(14, 165, 233, 0.2);
- }
-
- .output-section h3 {
- color: #0369a1;
- margin-bottom: 20px;
- font-size: 20px;
- font-weight: 600;
- display: flex;
- align-items: center;
- gap: 10px;
- }
-
- .output-section h3::before {
- content: '✨';
- font-size: 24px;
- }
-
- .arrow {
- text-align: center;
- color: #667eea;
- font-size: 32px;
- margin: -15px 0;
- position: relative;
- z-index: 1;
- filter: drop-shadow(0 2px 4px rgba(102, 126, 234, 0.3));
- }
-
- .arrow::before {
- content: '↓';
- }
-
- @media (max-width: 768px) {
- .container {
- padding: 10px;
- }
-
- .tab {
- padding: 12px 15px;
- font-size: 13px;
- }
-
- .tab-content {
- padding: 20px;
- }
- }
- </style>
- </head>
- <body>
- <div class="container">
- <h1>知识获取工作流可视化</h1>
- <div class="tabs" id="tabs">
- '''
-
- # 生成Tab标签
- for i, workflow in enumerate(workflows):
- question = workflow['input'].get('question', f'问题 {i+1}')
- active_class = 'active' if i == 0 else ''
- html += f' <button class="tab {active_class}" onclick="switchTab({i})">{escape_html(question)}</button>\n'
-
- html += ' </div>\n'
-
- # 生成Tab内容
- for i, workflow in enumerate(workflows):
- active_class = 'active' if i == 0 else ''
- html += f' <div class="tab-content {active_class}" id="tab-{i}">\n'
-
- # 输入信息
- html += ' <div class="input-section">\n'
- html += ' <h3>输入信息</h3>\n'
- html += f' <div class="input-item"><strong>问题:</strong> {escape_html(workflow["input"].get("question", ""))}</div>\n'
- post_info = workflow['input'].get('post_info', '')
- post_info_display = escape_html(post_info) if post_info else '<span class="placeholder">(无)</span>'
- html += f' <div class="input-item"><strong>帖子信息:</strong> {post_info_display}</div>\n'
- persona_info = workflow['input'].get('persona_info', '')
- persona_info_display = escape_html(persona_info) if persona_info else '<span class="placeholder">(无)</span>'
- html += f' <div class="input-item"><strong>人设信息:</strong> {persona_info_display}</div>\n'
- html += ' </div>\n'
-
- # 工作流程
- html += ' <div class="workflow">\n'
- for j, step in enumerate(workflow['steps']):
- step_id = f"step-{i}-{j}"
- html += f' <div class="workflow-step" id="{step_id}">\n'
- html += ' <div class="step-header" onclick="toggleStep(\'' + step_id + '\')">\n'
- html += ' <div class="step-title">\n'
- html += f' <span class="step-number">{j+1}</span>\n'
- html += f' <span class="step-name">{escape_html(step["name"])}</span>\n'
- html += ' </div>\n'
- html += ' <span class="step-toggle">▼</span>\n'
- html += ' </div>\n'
- html += ' <div class="step-content" id="content-' + step_id + '">\n'
-
- # 根据步骤类型显示不同内容(prompt放在最后,默认隐藏)
- prompt_id = f"prompt-{step_id}"
-
- if step['step'] == 'generate_query':
- if step.get('query'):
- html += ' <div class="step-detail">\n'
- html += ' <span class="step-detail-label">生成的Query:</span>\n'
- html += f' <div class="step-detail-content">{escape_html(step["query"])}</div>\n'
- html += ' </div>\n'
-
- elif step['step'] == 'select_tool':
- # 判断是否选择了工具
- if step.get('tool_name'):
- html += ' <div class="step-detail">\n'
- html += ' <span class="step-detail-label">工具名称:</span>\n'
- html += f' <div class="step-detail-content">{escape_html(step["tool_name"])}</div>\n'
- html += ' </div>\n'
- if step.get('tool_id'):
- html += ' <div class="step-detail">\n'
- html += ' <span class="step-detail-label">工具调用ID:</span>\n'
- html += f' <div class="step-detail-content">{escape_html(step["tool_id"])}</div>\n'
- html += ' </div>\n'
- if step.get('tool_usage'):
- html += ' <div class="step-detail">\n'
- html += ' <span class="step-detail-label">使用方法:</span>\n'
- html += f' <div class="step-detail-content">{escape_html(step["tool_usage"])}</div>\n'
- html += ' </div>\n'
- else:
- # 无工具选择时显示提示
- html += ' <div class="step-detail">\n'
- html += ' <span class="step-detail-label">选择结果:</span>\n'
- html += ' <div class="step-detail-content" style="color: #dc3545; font-weight: 500;">无匹配工具</div>\n'
- html += ' </div>\n'
-
- elif step['step'] == 'extract_params':
- if step.get('params'):
- html += ' <div class="step-detail">\n'
- html += ' <span class="step-detail-label">提取的参数:</span>\n'
- params_str = format_json_for_display(step['params'])
- html += f' <div class="step-detail-content json-content">{escape_html(params_str)}</div>\n'
- html += ' </div>\n'
-
- elif step['step'] == 'execute_tool':
- if step.get('response'):
- html += ' <div class="step-detail">\n'
- html += ' <span class="step-detail-label">执行结果:</span>\n'
- response_str = format_json_for_display(step['response'])
- html += f' <div class="step-detail-content json-content">{escape_html(response_str)}</div>\n'
- html += ' </div>\n'
-
- elif step['step'] == 'llm_search':
- search_results = step.get('search_results', [])
- if search_results:
- html += ' <div class="step-detail">\n'
- html += ' <span class="step-detail-label">搜索结果:</span>\n'
- for idx, result in enumerate(search_results, 1):
- query = result.get('query', '')
- content = result.get('content', '')
- html += ' <div style="margin-bottom: 15px; padding: 12px; background: #f0f8ff; border-radius: 6px; border-left: 3px solid #4a90e2;">\n'
- html += f' <div style="font-weight: 600; color: #2c3e50; margin-bottom: 8px;">查询 {idx}: {escape_html(query)}</div>\n'
- html += f' <div style="color: #555; line-height: 1.6; white-space: pre-wrap;">{escape_html(content)}</div>\n'
- html += ' </div>\n'
- html += ' </div>\n'
-
- elif step['step'] == 'multi_search_merge':
- if step.get('sources_count') is not None:
- html += ' <div class="step-detail">\n'
- html += ' <span class="step-detail-label">来源统计:</span>\n'
- html += f' <div class="step-detail-content">总来源数: {step.get("sources_count", 0)}, 有效来源数: {step.get("valid_sources_count", 0)}</div>\n'
- html += ' </div>\n'
- if step.get('response'):
- html += ' <div class="step-detail">\n'
- html += ' <span class="step-detail-label">整合结果:</span>\n'
- response_str = format_json_for_display(step['response'])
- html += f' <div class="step-detail-content">{escape_html(response_str)}</div>\n'
- html += ' </div>\n'
-
- # Prompt放在最后,默认隐藏
- if step.get('prompt'):
- html += f' <button class="prompt-toggle-btn" onclick="togglePrompt(\'{prompt_id}\')">显示 Prompt</button>\n'
- html += f' <div class="prompt-content" id="{prompt_id}">{escape_html(step["prompt"])}</div>\n'
-
- html += ' </div>\n'
- html += ' </div>\n'
-
- # 添加箭头(除了最后一步)
- if j < len(workflow['steps']) - 1:
- html += ' <div class="arrow"></div>\n'
-
- html += ' </div>\n'
-
- # 输出信息
- if workflow['output'].get('result'):
- html += ' <div class="output-section">\n'
- html += ' <h3>最终输出</h3>\n'
- result_str = format_json_for_display(workflow['output']['result'])
- html += f' <div class="step-detail-content json-content">{escape_html(result_str)}</div>\n'
- html += ' </div>\n'
-
- html += ' </div>\n'
-
- html += ''' </div>
-
- <script>
- function switchTab(index) {
- // 隐藏所有tab内容
- const contents = document.querySelectorAll('.tab-content');
- contents.forEach(content => content.classList.remove('active'));
-
- // 移除所有tab的active状态
- const tabs = document.querySelectorAll('.tab');
- tabs.forEach(tab => tab.classList.remove('active'));
-
- // 显示选中的tab内容
- document.getElementById('tab-' + index).classList.add('active');
- tabs[index].classList.add('active');
- }
-
- function toggleStep(stepId) {
- const step = document.getElementById(stepId);
- const content = document.getElementById('content-' + stepId);
- const toggle = step.querySelector('.step-toggle');
-
- if (content.classList.contains('expanded')) {
- content.classList.remove('expanded');
- toggle.classList.remove('expanded');
- step.classList.remove('active');
- } else {
- content.classList.add('expanded');
- toggle.classList.add('expanded');
- step.classList.add('active');
- }
- }
-
- function togglePrompt(promptId) {
- const promptContent = document.getElementById(promptId);
- const btn = promptContent.previousElementSibling;
-
- if (promptContent.classList.contains('show')) {
- promptContent.classList.remove('show');
- btn.textContent = '显示 Prompt';
- } else {
- promptContent.classList.add('show');
- btn.textContent = '隐藏 Prompt';
- }
- }
-
- // 页面加载时高亮第一个步骤
- window.addEventListener('load', function() {
- const firstSteps = document.querySelectorAll('.workflow-step');
- firstSteps.forEach((step, index) => {
- if (index === 0 || index % (firstSteps.length / document.querySelectorAll('.tab-content').length) === 0) {
- step.classList.add('active');
- const content = step.querySelector('.step-content');
- const toggle = step.querySelector('.step-toggle');
- if (content) {
- content.classList.add('expanded');
- toggle.classList.add('expanded');
- }
- }
- });
- });
- </script>
- </body>
- </html>'''
-
- return html
- def main():
- """主函数"""
- # 获取当前脚本所在目录
- script_dir = Path(__file__).parent
- os.chdir(script_dir)
-
- # 读取数据文件
- print("正在读取数据文件...")
- data_list = load_data_files(DATA_FILE_PATHS)
-
- if not data_list:
- print("错误: 没有成功读取任何数据文件")
- return
-
- print(f"成功读取 {len(data_list)} 个数据文件")
-
- # 解析工作流数据
- print("正在解析工作流数据...")
- workflows = [parse_workflow_data(data) for data in data_list]
-
- # 生成HTML
- print("正在生成HTML页面...")
- html = generate_html(workflows)
-
- # 保存HTML文件
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
- output_filename = f"workflow_visualization_{timestamp}.html"
-
- with open(output_filename, 'w', encoding='utf-8') as f:
- f.write(html)
-
- print(f"HTML页面已生成: {output_filename}")
- print(f"文件路径: {os.path.abspath(output_filename)}")
- if __name__ == '__main__':
- main()
|