guoshuqi1212
修改指南格式
af701ab
# app.py - 添加双重验证系统的完整版本
import gradio as gr
import json
import os
import html
from huggingface_hub import HfApi, hf_hub_download
# --- Configuration ---
HF_TOKEN_ENV_VAR = "HF_TOKEN"
GRADIO_USERNAME_ENV_VAR = "GRADIO_USERNAME"
GRADIO_PASSWORD_ENV_VAR = "GRADIO_PASSWORD"
DATA_REPO_ID = "liveclinreview/translated_liveClin"
RESULTS_REPO_ID = "liveclinreview/review_check_data"
RESULTS_FILENAME_IN_REPO = "review_results.json"
MAX_MCQS_PER_FILE = 30
# --- 辅助函数 ---
def get_translated_text(data_dict, key, fallback_key=None, default_value="N/A"):
"""优先获取翻译文本,无翻译时回退到原文"""
if not isinstance(data_dict, dict):
return default_value
translated_key = f"{key}_translated"
if translated_key in data_dict and data_dict[translated_key] is not None and data_dict[translated_key].strip():
return data_dict[translated_key]
if key in data_dict and data_dict[key] is not None:
return data_dict[key]
if fallback_key:
translated_fallback_key = f"{fallback_key}_translated"
if translated_fallback_key in data_dict and data_dict[translated_fallback_key] is not None and data_dict[translated_fallback_key].strip():
return data_dict[translated_fallback_key]
if fallback_key in data_dict and data_dict[fallback_key] is not None:
return data_dict[fallback_key]
return default_value
def get_translated_caption(img_data_dict, default_value="图片说明不可用"):
"""获取图片翻译说明"""
if not isinstance(img_data_dict, dict):
return default_value
translated_keys_to_try = [
"image_detail_translated",
"image_caption_translated",
"caption_translated",
"image_caption_with_id_translated"
]
for key in translated_keys_to_try:
if key in img_data_dict and img_data_dict[key] is not None and img_data_dict[key].strip():
return img_data_dict[key]
original_keys_to_try = [
"image_detail", "image_caption", "caption", "image_caption_with_id"
]
for key in original_keys_to_try:
if key in img_data_dict and img_data_dict[key] is not None:
return img_data_dict[key]
return default_value
def resolve_image_url(img_data_dict):
"""从图片数据字典中解析图片URL"""
if not isinstance(img_data_dict, dict):
return None
url = img_data_dict.get('url')
if url and url.strip():
return url.strip()
for alt_key in ['image_url', 'src', 'href']:
alt_url = img_data_dict.get(alt_key)
if alt_url and alt_url.strip():
return alt_url.strip()
return None
def create_error_data(error_msg):
"""创建错误数据结构"""
return {
"title": f"Error: {error_msg}",
"title_translated": f"错误: {error_msg}",
"case_text": "无法加载案例数据",
"case_text_translated": "无法加载案例数据",
"discussion_text": "无法加载讨论数据",
"discussion_text_translated": "无法加载讨论数据",
"image_details": [],
"table_details": [],
"exam_creation": {"final_policy": {"mcqs": []}}
}
# --- Hugging Face 数据交互函数 ---
def find_json_files_from_hf():
"""从预生成的索引文件快速获取文件列表"""
hf_token = os.environ.get(HF_TOKEN_ENV_VAR)
if not hf_token:
print("Warning: 未找到HF_TOKEN,无法列出文件。")
return []
try:
print("正在下载文件索引...")
index_path = hf_hub_download(
repo_id=DATA_REPO_ID,
filename="file_index.json",
repo_type="dataset",
token=hf_token
)
with open(index_path, 'r', encoding='utf-8') as f:
json_files = json.load(f)
print(f"从索引找到 {len(json_files)} 个JSON文件")
return json_files
except Exception as e:
print(f"下载文件索引失败: {e}")
return []
def load_case_data_from_hf(path_in_repo):
"""从Hugging Face数据集加载案例数据"""
if not path_in_repo:
return create_error_data("无效的文件路径"), "INVALID_PATH"
hf_token = os.environ.get(HF_TOKEN_ENV_VAR)
if not hf_token:
return create_error_data("未找到HF token"), path_in_repo
try:
print(f"正在下载文件: {path_in_repo}")
downloaded_file_path = hf_hub_download(
repo_id=DATA_REPO_ID,
filename=path_in_repo,
repo_type="dataset",
token=hf_token
)
with open(downloaded_file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
print(f"文件加载成功: {path_in_repo}")
return data, path_in_repo
except Exception as e:
print(f"加载文件失败 {path_in_repo}: {e}")
return create_error_data(f"加载失败: {e}"), path_in_repo
def load_results_from_hf():
"""从Hugging Face数据集加载已保存的审核结果"""
hf_token = os.environ.get(HF_TOKEN_ENV_VAR)
if not hf_token:
print("Warning: HF_TOKEN 未设置,无法加载审核结果。")
return {}
try:
print(f"正在下载审核结果...")
downloaded_path = hf_hub_download(
repo_id=RESULTS_REPO_ID,
filename=RESULTS_FILENAME_IN_REPO,
repo_type="dataset",
token=hf_token,
)
with open(downloaded_path, 'r', encoding='utf-8') as f:
results = json.load(f)
print("审核结果加载成功")
return results
except Exception as e:
print(f"无法加载审核结果文件: {e}")
return {}
def save_results_to_hf(data_to_save):
"""保存审核结果到Hugging Face数据集"""
hf_token = os.environ.get(HF_TOKEN_ENV_VAR)
if not hf_token:
msg = "Warning: HF_TOKEN 未设置,无法保存审核结果。"
print(msg)
return msg, data_to_save
temp_file_path = "temp_review_results.json"
try:
with open(temp_file_path, 'w', encoding='utf-8') as f:
json.dump(data_to_save, f, indent=2, ensure_ascii=False)
api = HfApi(token=hf_token)
api.upload_file(
path_or_fileobj=temp_file_path,
path_in_repo=RESULTS_FILENAME_IN_REPO,
repo_id=RESULTS_REPO_ID,
repo_type="dataset",
commit_message="Update review results"
)
msg = f"结果已成功保存到 HF Dataset: {RESULTS_REPO_ID}"
print(msg)
return msg, data_to_save
except Exception as e:
msg = f"保存结果到 HF Dataset 时出错: {e}"
print(msg)
return msg, data_to_save
finally:
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
# --- Critic Text ---
def get_critic_evaluation_text(case_data_dict, mcq_index):
if not case_data_dict or not isinstance(case_data_dict.get("exam_creation"), dict):
return "Evaluation data missing or invalid."
exam_creation = case_data_dict["exam_creation"]
final_policy = exam_creation.get("final_policy", {})
critic_data = None
finalized_round_num = final_policy.get("finalized_from_round")
if finalized_round_num:
potential_key = f"round{finalized_round_num}_critic"
if potential_key in exam_creation and isinstance(exam_creation[potential_key], dict):
critic_data = exam_creation[potential_key]
if critic_data is None:
for i in range(3, 0, -1):
potential_key = f"round{i}_critic"
if potential_key in exam_creation and isinstance(exam_creation[potential_key], dict):
potential_critiques = exam_creation[potential_key].get("critiques")
if isinstance(potential_critiques, list) and potential_critiques:
critic_data = exam_creation[potential_key]
break
if critic_data is None and isinstance(final_policy.get("critiques"), list) and final_policy["critiques"]:
critiques_list_in_final_policy = final_policy["critiques"]
for critique_obj in critiques_list_in_final_policy:
current_critique_mcq_idx = critique_obj.get("original_mcq_index")
if current_critique_mcq_idx is None and isinstance(critique_obj.get("critique"), dict):
current_critique_mcq_idx = critique_obj["critique"].get("mcq_index")
if current_critique_mcq_idx == mcq_index:
actual_critique_content = critique_obj.get("critique", {})
if isinstance(actual_critique_content, dict):
correctness_eval = actual_critique_content.get("correctness_evaluation", {})
suggestions = get_translated_text(correctness_eval, "critique_and_suggestions", default_value=f"Evaluation for MCQ {mcq_index + 1} in final_policy found, but content is empty or invalid.")
return f"{suggestions}"
else:
return str(actual_critique_content)
return f"Could not find specific evaluation for question {mcq_index + 1} in final_policy's critique list."
if critic_data:
critiques = critic_data.get("critiques", [])
if not isinstance(critiques, list):
return f"Invalid 'critiques' format in the selected critic data."
for critique_obj in critiques:
current_critique_mcq_idx = critique_obj.get("original_mcq_index")
if current_critique_mcq_idx is None and isinstance(critique_obj.get("critique"), dict):
current_critique_mcq_idx = critique_obj["critique"].get("mcq_index")
if current_critique_mcq_idx == mcq_index:
actual_critique_content = critique_obj.get("critique", {})
if isinstance(actual_critique_content, dict):
correctness_eval = actual_critique_content.get("correctness_evaluation", {})
suggestions = get_translated_text(correctness_eval, "critique_and_suggestions", default_value="评价内容为空。")
return f"{suggestions}"
else:
return str(actual_critique_content)
return f"找到了评价数据,但没有找到问题 {mcq_index + 1} 的具体条目。"
return "未找到对应的模型评价数据(例如 roundX_critic)。"
def get_reassessment_text(case_data_dict, mcq_index):
"""获取模型再评估文本"""
if not case_data_dict or not isinstance(case_data_dict.get("quality_assessment"), dict):
return "再评估数据不可用"
qa = case_data_dict["quality_assessment"]
assessment_structured = qa.get("assessment_structured", {})
if not assessment_structured:
return "再评估数据不可用"
# MCQ索引从0开始,但显示时是从1开始,所以要+1
mcq_key = f"mcq{mcq_index + 1}"
# 优先获取中文版本
chinese_version = assessment_structured.get("chinese", {})
if mcq_key in chinese_version:
text = chinese_version[mcq_key]
# 在 bullet point 前添加换行
return text.replace("•", "\n•")
# 如果没有中文版本,尝试英文版本
english_version = assessment_structured.get("english", {})
if mcq_key in english_version:
text = english_version[mcq_key]
# 在 bullet point 前添加换行
return text.replace("•", "\n•")
return f"未找到问题 {mcq_index + 1} 的再评估数据"
def get_overall_reassessment_text(case_data_dict):
"""获取整体再评估总结文本"""
if not case_data_dict or not isinstance(case_data_dict.get("quality_assessment"), dict):
return ""
qa = case_data_dict["quality_assessment"]
assessment_structured = qa.get("assessment_structured", {})
if not assessment_structured:
return ""
# 优先获取中文版本
chinese_version = assessment_structured.get("chinese", {})
if "overall" in chinese_version:
return chinese_version["overall"]
# 如果没有中文版本,尝试英文版本
english_version = assessment_structured.get("english", {})
if "overall" in english_version:
return english_version["overall"]
return ""
# --- UI Update Logic ---
def update_static_content(case_data_dict, current_json_path):
is_error = not case_data_dict or "Error:" in case_data_dict.get("title", "")
if is_error:
err_title = get_translated_text(case_data_dict, "title", default_value="加载数据时出错")
err_text = get_translated_text(case_data_dict, "case_text", fallback_key="abstract", default_value="无法加载或解析案例数据。")
return {
"status_message_md": gr.update(value=f"**加载文件 {os.path.basename(current_json_path)} 时出错:** {err_text}", visible=True),
"case_display_area": gr.update(visible=False),
"title_md": gr.update(value=""),
"level_info_md": gr.update(value="", visible=False),
"ids_md": gr.update(value=""),
"case_text_md": gr.update(value=""),
"discussion_text_md": gr.update(value=""),
"general_tables_display_md": gr.update(value="", visible=False),
"single_general_img": gr.update(value=None, visible=False),
"single_general_caption_md": gr.update(value="", visible=False),
"general_images_gallery_multi": gr.update(value=[], visible=False),
"general_images_captions_md": gr.update(value="", visible=False),
"quality_assessment_md": gr.update(value="", visible=False),
"scoring_summary_md": gr.update(value="", visible=False),
"scenario_text_md": gr.update(value=""),
"single_scenario_img": gr.update(value=None, visible=False),
"single_scenario_caption_md": gr.update(value="", visible=False),
"scenario_images_gallery": gr.update(value=[], visible=False),
"scenario_images_captions_md": gr.update(value="", visible=False),
"overall_reassessment_accordion": gr.update(visible=False), # 添加
"overall_reassessment_text": gr.update(value=""),
}
updates = {
"status_message_md": gr.update(value="案例加载成功。", visible=False),
"case_display_area": gr.update(visible=True),
"title_md": gr.update(value=f"# {get_translated_text(case_data_dict, 'title', default_value='N/A')}"),
"ids_md": gr.update(value=f"**DOI:** {case_data_dict.get('doi', 'N/A')}"),
"case_text_md": gr.update(value=get_translated_text(case_data_dict, 'case_text', default_value='案例文本不可用。')),
"discussion_text_md": gr.update(value=get_translated_text(case_data_dict, 'discussion_text', default_value='讨论部分不可用。')),
}
# Level信息
level_parts = []
level1 = get_translated_text(case_data_dict, 'Level1', default_value="")
level2 = get_translated_text(case_data_dict, 'Level2', default_value="")
level2_detail = get_translated_text(case_data_dict, 'Level2_Detail', default_value="")
if level1 and level1 != "N/A":
level_parts.append(f"**分类级别1:** {html.escape(level1)}")
if level2 and level2 != "N/A":
level_parts.append(f"**分类级别2:** {html.escape(level2)}")
if level2_detail and level2_detail != "N/A":
level_parts.append(f"**详细分类:** {html.escape(level2_detail)}")
level_info_text = "\n\n".join(level_parts) if level_parts else ""
updates["level_info_md"] = gr.update(value=level_info_text, visible=bool(level_parts))
# 处理表格
general_tables_parts = []
if case_data_dict.get('table_details'):
for tbl in case_data_dict['table_details']:
if isinstance(tbl, dict):
caption = get_translated_text(tbl, 'caption', default_value='表格')
content = get_translated_text(tbl, 'table_translated', fallback_key='table', default_value='表格内容为空。')
general_tables_parts.append(f"### {html.escape(caption)}")
general_tables_parts.append(content)
updates["general_tables_display_md"] = gr.update(value="\n\n".join(general_tables_parts), visible=bool(general_tables_parts))
# 处理通用图片
general_images_list = case_data_dict.get('image_details', [])
if len(general_images_list) == 1:
img_data = general_images_list[0]
img_url = resolve_image_url(img_data)
caption = get_translated_caption(img_data, default_value='案例图片')
updates["single_general_img"] = gr.update(value=img_url, visible=bool(img_url))
updates["single_general_caption_md"] = gr.update(value=f"<p style='text-align:center; font-style:italic; margin-top:0px;'>{html.escape(caption)}</p>", visible=bool(img_url))
updates["general_images_gallery_multi"] = gr.update(value=[], visible=False)
updates["general_images_captions_md"] = gr.update(value="", visible=False)
elif len(general_images_list) > 1:
gallery_items = []
captions_list_md_parts = ["**图片说明详情:**"]
for idx, img_data in enumerate(general_images_list):
if not isinstance(img_data, dict):
continue
img_url = resolve_image_url(img_data)
short_caption_for_gallery = img_data.get('caption', f'图片 {idx+1}')
long_translated_caption = get_translated_caption(img_data, default_value=f'图片 {idx+1}')
if img_url:
gallery_items.append((img_url, html.escape(short_caption_for_gallery)))
captions_list_md_parts.append(f"{idx+1}. {html.escape(long_translated_caption)}")
updates["single_general_img"] = gr.update(value=None, visible=False)
updates["single_general_caption_md"] = gr.update(value="", visible=False)
updates["general_images_gallery_multi"] = gr.update(value=gallery_items, visible=bool(gallery_items))
updates["general_images_captions_md"] = gr.update(value="\n".join(captions_list_md_parts), visible=bool(gallery_items))
else:
updates["single_general_img"] = gr.update(value=None, visible=False)
updates["single_general_caption_md"] = gr.update(value="", visible=False)
updates["general_images_gallery_multi"] = gr.update(value=[], visible=False)
updates["general_images_captions_md"] = gr.update(value="", visible=False)
# 质量评估
quality_parts = []
if case_data_dict.get('quality_assessment'):
qa = case_data_dict['quality_assessment']
assessment_text = get_translated_text(qa, 'assessment', default_value="")
if assessment_text and assessment_text.strip():
quality_parts.append("### 质量评估")
quality_parts.append(assessment_text)
updates["quality_assessment_md"] = gr.update(
value="\n\n".join(quality_parts) if quality_parts else "",
visible=bool(quality_parts) # 只有有内容时才显示
)
# 评分信息
scoring_parts = []
if case_data_dict.get('scoring'):
scoring = case_data_dict['scoring']
# 严重缺陷 - 只有当details有内容时才显示
critical_flaws = scoring.get('critical_flaws', {})
if isinstance(critical_flaws, dict):
details = critical_flaws.get('details', [])
if isinstance(details, list) and details: # 只有details非空时才处理
count = critical_flaws.get('count', len(details))
scoring_parts.append(f"### 严重缺陷 (数量: {count})")
for i, detail in enumerate(details):
detail_text = ""
if isinstance(detail, dict):
# 优先使用 details_translated
detail_text = detail.get('details_translated', '').strip()
if not detail_text:
detail_text = detail.get('details', '').strip()
elif isinstance(detail, str):
detail_text = detail.strip()
if detail_text: # 只添加非空的detail
scoring_parts.append(f"{i+1}. {detail_text}")
# 次要问题 - 只有当details有内容时才显示
minor_issues = scoring.get('minor_issues', {})
if isinstance(minor_issues, dict):
details = minor_issues.get('details', [])
if isinstance(details, list) and details: # 只有details非空时才处理
count = minor_issues.get('count', len(details))
if scoring_parts: # 如果前面有内容,添加空行
scoring_parts.append("")
scoring_parts.append(f"### 次要问题 (数量: {count})")
for i, detail in enumerate(details):
detail_text = ""
if isinstance(detail, dict):
# 优先使用 details_translated
detail_text = detail.get('details_translated', '').strip()
if not detail_text:
detail_text = detail.get('details', '').strip()
elif isinstance(detail, str):
detail_text = detail.strip()
if detail_text: # 只添加非空的detail
scoring_parts.append(f"{i+1}. {detail_text}")
# 总体评价
overall_summary = get_translated_text(scoring, 'overall_summary', default_value="")
if overall_summary and overall_summary.strip():
if scoring_parts: # 如果前面有内容,添加空行
scoring_parts.append("")
scoring_parts.append("### 总体评价")
scoring_parts.append(overall_summary)
updates["scoring_summary_md"] = gr.update(
value="\n".join(scoring_parts) if scoring_parts else "",
visible=bool(scoring_parts) # 只有有内容时才显示
)
# 处理场景信息
exam_creation_data = case_data_dict.get('exam_creation', {})
final_policy_data = exam_creation_data.get('final_policy', {})
scenario_text = get_translated_text(final_policy_data, 'new_scenario', fallback_key='scenario', default_value='临床场景描述不可用。')
updates["scenario_text_md"] = gr.update(value=scenario_text)
# 处理场景图片
scenario_images_list = final_policy_data.get('scenario_image_details', [])
if not isinstance(scenario_images_list, list):
scenario_images_list = []
if len(scenario_images_list) == 1:
img_data = scenario_images_list[0]
img_url = resolve_image_url(img_data)
caption = get_translated_caption(img_data, default_value='场景图片')
updates["single_scenario_img"] = gr.update(value=img_url, visible=bool(img_url))
updates["single_scenario_caption_md"] = gr.update(value=f"<p style='text-align:center; font-style:italic; margin-top:0px;'>{html.escape(caption)}</p>", visible=bool(img_url))
updates["scenario_images_gallery"] = gr.update(value=[], visible=False)
updates["scenario_images_captions_md"] = gr.update(value="", visible=False)
elif len(scenario_images_list) > 1:
gallery_items = []
captions_list_md_parts = ["**图片说明详情:**"]
for idx, img_data in enumerate(scenario_images_list):
if not isinstance(img_data, dict):
continue
img_url = resolve_image_url(img_data)
full_caption = get_translated_caption(img_data, default_value=f'场景图片 {idx+1}')
if img_url:
gallery_items.append((img_url, html.escape(full_caption)))
captions_list_md_parts.append(f"{idx+1}. {html.escape(full_caption)}")
updates["single_scenario_img"] = gr.update(value=None, visible=False)
updates["single_scenario_caption_md"] = gr.update(value="", visible=False)
updates["scenario_images_gallery"] = gr.update(value=gallery_items, visible=bool(gallery_items))
updates["scenario_images_captions_md"] = gr.update(value="\n".join(captions_list_md_parts), visible=bool(gallery_items))
else:
updates["single_scenario_img"] = gr.update(value=None, visible=False)
updates["single_scenario_caption_md"] = gr.update(value="", visible=False)
updates["scenario_images_gallery"] = gr.update(value=[], visible=False)
updates["scenario_images_captions_md"] = gr.update(value="", visible=False)
overall_text = get_overall_reassessment_text(case_data_dict)
updates["overall_reassessment_accordion"] = gr.update(visible=bool(overall_text.strip()))
updates["overall_reassessment_text"] = gr.update(value=overall_text)
return updates
def update_mcq_components(case_data_dict, current_json_path, current_results):
mcq_updates = {}
mcqs_data = []
if case_data_dict and isinstance(case_data_dict.get("exam_creation"), dict):
final_policy = case_data_dict['exam_creation'].get('final_policy', {})
mcqs_data = final_policy.get('mcqs', [])
if not isinstance(mcqs_data, list):
mcqs_data = []
num_mcqs = len(mcqs_data)
file_results = current_results.get(current_json_path, {})
for i in range(MAX_MCQS_PER_FILE):
mcq_slot = mcq_output_components[i]
if i < num_mcqs:
current_mcq_item_data = mcqs_data[i]
mcq_idx = i
# 使用翻译文本
stage_text = get_translated_text(current_mcq_item_data, 'stage', default_value='N/A')
header_text = f"### 问题 {mcq_idx + 1} / {num_mcqs} <small>({stage_text})</small>"
mcq_updates[mcq_slot["header"]] = gr.update(value=header_text)
question_text = get_translated_text(current_mcq_item_data, 'question', default_value='问题文本为空。')
mcq_updates[mcq_slot["question"]] = gr.update(value=question_text)
# 处理选项
options = current_mcq_item_data.get('options_translated', current_mcq_item_data.get('options', {}))
options_text_parts = ["**选项:**"]
if options and isinstance(options, dict):
original_keys = sorted([k for k in options.keys() if len(k) == 1 and k.isalpha()])
for key in original_keys:
option_text = options.get(key, '')
options_text_parts.append(f"- {key}. {html.escape(str(option_text))}")
else:
options_text_parts.append("未提供选项。")
mcq_updates[mcq_slot["options"]] = gr.update(value="\n".join(options_text_parts))
# 正确答案
correct_ans_key = current_mcq_item_data.get('correct_answer', 'N/A')
correct_ans_text = options.get(correct_ans_key, "未提供") if isinstance(options, dict) else "未提供"
answer_text = f"**正确答案:** {correct_ans_key}. {html.escape(str(correct_ans_text))}"
mcq_updates[mcq_slot["answer"]] = gr.update(value=answer_text)
# 处理MCQ表格
mcq_tables_parts = []
mcq_tables_list = current_mcq_item_data.get('table_details', [])
if isinstance(mcq_tables_list, list):
for tbl_data in mcq_tables_list:
if isinstance(tbl_data, dict):
caption = get_translated_text(tbl_data, 'caption', default_value='相关表格')
content = get_translated_text(tbl_data, 'table_translated', fallback_key='table', default_value='表格内容为空。')
mcq_tables_parts.append(f"#### {html.escape(caption)}")
mcq_tables_parts.append(content)
mcq_updates[mcq_slot["tables"]] = gr.update(value="\n\n".join(mcq_tables_parts), visible=bool(mcq_tables_parts))
# 处理MCQ图片
mcq_img_list = current_mcq_item_data.get('image_details', [])
if not isinstance(mcq_img_list, list):
mcq_img_list = []
if len(mcq_img_list) == 1:
img_data = mcq_img_list[0]
img_url = resolve_image_url(img_data)
caption = get_translated_caption(img_data, default_value=f'问题 {mcq_idx + 1} 图片')
mcq_updates[mcq_slot["single_img"]] = gr.update(value=img_url, visible=bool(img_url))
mcq_updates[mcq_slot["single_img_caption"]] = gr.update(value=f"<p style='text-align:center; font-style:italic; margin-top:0px;'>{html.escape(caption)}</p>", visible=bool(img_url))
mcq_updates[mcq_slot["gallery_img"]] = gr.update(value=[], visible=False)
mcq_updates[mcq_slot["gallery_img_caption"]] = gr.update(value="", visible=False)
elif len(mcq_img_list) > 1:
gallery_items = []
captions_list_md_parts = ["**图片说明详情:**"]
for i_img, img_data in enumerate(mcq_img_list):
if not isinstance(img_data, dict):
continue
img_url = resolve_image_url(img_data)
full_caption = get_translated_caption(img_data, default_value=f'问题 {mcq_idx + 1} 图片 {i_img+1}')
if img_url:
gallery_items.append((img_url, html.escape(full_caption)))
captions_list_md_parts.append(f"{i_img+1}. {html.escape(full_caption)}")
mcq_updates[mcq_slot["single_img"]] = gr.update(value=None, visible=False)
mcq_updates[mcq_slot["single_img_caption"]] = gr.update(value="", visible=False)
mcq_updates[mcq_slot["gallery_img"]] = gr.update(value=gallery_items, visible=bool(gallery_items))
mcq_updates[mcq_slot["gallery_img_caption"]] = gr.update(value="\n".join(captions_list_md_parts), visible=bool(gallery_items))
else:
mcq_updates[mcq_slot["single_img"]] = gr.update(value=None, visible=False)
mcq_updates[mcq_slot["single_img_caption"]] = gr.update(value="", visible=False)
mcq_updates[mcq_slot["gallery_img"]] = gr.update(value=[], visible=False)
mcq_updates[mcq_slot["gallery_img_caption"]] = gr.update(value="", visible=False)
# 评价文本
critic_text = get_critic_evaluation_text(case_data_dict, mcq_idx)
mcq_updates[mcq_slot["critic_text"]] = gr.update(value=critic_text)
mcq_updates[mcq_slot["critic_accordion"]] = gr.update(open=True)
# 添加再评估文本
reassessment_text = get_reassessment_text(case_data_dict, mcq_idx)
mcq_updates[mcq_slot["reassessment_text"]] = gr.update(value=reassessment_text)
mcq_updates[mcq_slot["reassessment_accordion"]] = gr.update(open=True)
# 加载保存的双重验证答案
saved_mcq_data = file_results.get(str(mcq_idx), {})
if isinstance(saved_mcq_data, dict):
# 事实验证
factual_check = saved_mcq_data.get("factual_check")
factual_comment = saved_mcq_data.get("factual_comment", "")
mcq_updates[mcq_slot["factual_radio"]] = gr.update(value=factual_check if factual_check in ["正确", "错误"] else None)
mcq_updates[mcq_slot["factual_comment"]] = gr.update(value=factual_comment, visible=factual_check == "错误")
# 逻辑可解性
logical_check = saved_mcq_data.get("logical_check")
logical_comment = saved_mcq_data.get("logical_comment", "")
mcq_updates[mcq_slot["logical_radio"]] = gr.update(value=logical_check if logical_check in ["是", "否"] else None)
mcq_updates[mcq_slot["logical_comment"]] = gr.update(value=logical_comment, visible=logical_check == "否")
else:
# 如果是旧数据格式或无数据
mcq_updates[mcq_slot["factual_radio"]] = gr.update(value=None)
mcq_updates[mcq_slot["factual_comment"]] = gr.update(value="", visible=False)
mcq_updates[mcq_slot["logical_radio"]] = gr.update(value=None)
mcq_updates[mcq_slot["logical_comment"]] = gr.update(value="", visible=False)
mcq_updates[mcq_slot["verification_accordion"]] = gr.update(visible=True)
mcq_updates[mcq_slot["group"]] = gr.update(visible=True)
else:
mcq_updates[mcq_slot["group"]] = gr.update(visible=False)
# 添加新组件的隐藏更新
mcq_updates[mcq_slot["reassessment_accordion"]] = gr.update(visible=False)
mcq_updates[mcq_slot["reassessment_text"]] = gr.update(value="")
mcq_updates[mcq_slot["verification_accordion"]] = gr.update(visible=False)
mcq_updates[mcq_slot["factual_radio"]] = gr.update(value=None)
mcq_updates[mcq_slot["factual_comment"]] = gr.update(value="", visible=False)
mcq_updates[mcq_slot["logical_radio"]] = gr.update(value=None)
mcq_updates[mcq_slot["logical_comment"]] = gr.update(value="", visible=False)
return mcq_updates
def collect_current_page_answers(current_filepath, current_results, *verification_values):
"""收集当前页面的双重验证答案"""
if not current_filepath or current_filepath == "INVALID_PATH":
print("Cannot collect answers: Invalid file path state.")
return current_results
updated_results = current_results.copy()
if current_filepath not in updated_results:
updated_results[current_filepath] = {}
# 每个MCQ有4个值:factual_radio, factual_comment, logical_radio, logical_comment
values_per_mcq = 4
num_mcqs = len(verification_values) // values_per_mcq
for i in range(min(num_mcqs, MAX_MCQS_PER_FILE)):
base_idx = i * values_per_mcq
factual_radio = verification_values[base_idx]
factual_comment = verification_values[base_idx + 1]
logical_radio = verification_values[base_idx + 2]
logical_comment = verification_values[base_idx + 3]
# 只有当至少有一个评分时才保存
if factual_radio in ["正确", "错误"] or logical_radio in ["是", "否"]:
updated_results[current_filepath][str(i)] = {
"factual_check": factual_radio if factual_radio in ["正确", "错误"] else None,
"factual_comment": factual_comment if factual_comment else "",
"logical_check": logical_radio if logical_radio in ["是", "否"] else None,
"logical_comment": logical_comment if logical_comment else ""
}
elif str(i) in updated_results[current_filepath]:
# 如果两个都没有选择,删除该记录
del updated_results[current_filepath][str(i)]
return updated_results
# --- CSS ---
custom_css = """
body, .gradio-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif !important;
}
.mcq-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 15px;
margin: 10px 0;
background-color: #fafafa;
}
.nav-footer {
margin-top: 20px;
padding: 15px;
background-color: #f5f5f5;
border-radius: 8px;
}
.nav-info {
color: #666;
font-style: italic;
margin: 5px 0;
}
.save-status {
margin-top: 10px;
padding: 8px;
border-radius: 4px;
}
.caption {
font-size: 0.9em;
color: #666;
margin-top: 5px;
}
.guideline-card {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
border: 2px solid #4a90e2;
border-radius: 12px;
padding: 20px;
margin: 15px 0;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.guideline-header {
color: #2c3e50;
font-size: 1.3em;
font-weight: bold;
margin-bottom: 15px;
text-align: center;
}
.guideline-content {
background: white;
border-radius: 8px;
padding: 15px;
line-height: 1.6;
}
.guideline-section {
margin: 10px 0;
}
.guideline-emphasis {
color: #e74c3c;
font-weight: bold;
}
/* Accordion标题样式优化 */
.verification-accordion .label-wrap {
font-weight: bold !important;
font-size: 1.1em !important;
color: #2c3e50 !important;
}
/* 如果使用无边框设计 */
.borderless-accordion {
border: none !important;
box-shadow: none !important;
}
/* 提示文字样式 */
.small-hint {
font-size: 0.85em !important;
color: #666 !important;
font-style: italic !important;
margin-top: -10px !important;
margin-bottom: 10px !important;
}
/* Accordion容器样式统一 */
.critic-accordion, .reassessment-accordion, .verification-accordion {
border: 1px solid #e0e0e0 !important;
border-radius: 8px !important;
margin: 5px 0 !important;
}
"""
# --- Gradio App ---
# 先定义MCQ组件,以便其他函数可以引用
mcq_output_components = []
verification_inputs_list = []
with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", secondary_hue="indigo"), title="医疗案例审查系统", css=custom_css) as app:
# --- State ---
all_json_files_state = gr.State(value=[])
current_file_index_state = gr.State(value=-1)
current_data_state = gr.State(value={})
current_filepath_state = gr.State(value="NOT_LOADED")
results_state = gr.State(value={})
# --- UI Layout ---
gr.Markdown("# 医疗案例审查与问题评估系统")
# 添加审核指南展示
with gr.Column(elem_classes=["guideline-card"]):
gr.Markdown("## 📋 审核指南", elem_classes=["guideline-header"])
with gr.Accordion("📖 详细审核指南(点击折叠/展开)", open=False): # open=False 表示默认折叠
with gr.Column(elem_classes=["guideline-content"]):
gr.Markdown("""
### 审核员身份定位
您是医学案例问题答案验证员。您的任务是对每个问题进行双重严格验证。
#### 信息介绍
* **特权信息 (考生不可见):** 源案例报告 (案例描述 + 讨论);图片详细说明文字;作为事实验证。
* **考生可见信息:** 当前问题 + 选项;图片 (无文字说明);历史题目记录;作为逻辑推理依据。
#### 验证流程
* **检查1: 事实验证 (基于特权信息)**
* **核心问题:** “这个题目在医学事实上是否准确?”
* **具体关键:** 将问题内容与源案例报告对比,验证医学数据、前提和答案的准确性。
* **检查2: 逻辑可解性 (基于考生可见信息)**
* **核心问题:** “仅凭可见信息,能否通过逻辑推理找到答案?”
* **具体关键:** 仅基于临床场景、题目、图片 (无说明),判断逻辑推理路径是否清晰,答案是否唯一。
#### 操作要求
* 每个问题独立完成两项检查。
* 选择 **“错误”** 或 **“否”** 时,需要详细说明原因和提供具体的改进建议。
#### 重要提醒
* 两个检查角度 **完全独立**,避免信息混淆。
* 以 **考生视角** 为核心评判逻辑可解性。
* AI 参考建议 **仅供参考**。
""")
status_message_md = gr.Markdown(visible=False)
with gr.Column(visible=False) as case_display_area:
title_md = gr.Markdown()
level_info_md = gr.Markdown(visible=False)
ids_md = gr.Markdown()
with gr.Accordion("源案例报告(题目依据,被测试者不可见)", open=True):
with gr.Tab("案例描述"):
case_text_md = gr.Markdown()
with gr.Tab("案例讨论"):
discussion_text_md = gr.Markdown()
with gr.Accordion("案例通用图表与图片(总)", open=False):
general_tables_display_md = gr.Markdown(visible=False)
single_general_img = gr.Image(label="案例图片", visible=False, show_label=False, interactive=False, height=300)
single_general_caption_md = gr.Markdown(visible=False, elem_classes=["caption"])
general_images_gallery_multi = gr.Gallery(label="案例图片 (多张)", show_label=False, columns=3, object_fit="contain", height="auto", visible=False)
general_images_captions_md = gr.Markdown(label="图片说明列表", visible=False)
# 在这里添加新的 Accordion
with gr.Accordion("裁判智能体(o3)(总)", open=False):
quality_assessment_md = gr.Markdown(visible=False)
scoring_summary_md = gr.Markdown(visible=False)
gr.HTML("<hr style='margin: 20px 0;'>")
gr.Markdown("## 临床场景")
with gr.Row(equal_height=False):
with gr.Column(scale=3):
scenario_text_md = gr.Markdown()
with gr.Column(scale=2, min_width=300):
single_scenario_img = gr.Image(label="场景图片", visible=False, show_label=False, interactive=False, height=300)
single_scenario_caption_md = gr.Markdown(visible=False, elem_classes=["caption"])
scenario_images_gallery = gr.Gallery(label="场景图片", show_label=False, columns=1, object_fit="contain", height="auto", visible=False)
scenario_images_captions_md = gr.Markdown(label="图片说明列表", visible=False)
gr.HTML("<hr style='margin: 20px 0;'>")
gr.Markdown("## 评估问题")
with gr.Column() as mcq_dynamic_container:
for i in range(MAX_MCQS_PER_FILE):
slot_components = {}
with gr.Group(visible=False, elem_classes=["mcq-card"]) as mcq_group:
slot_components["group"] = mcq_group
slot_components["header"] = gr.Markdown(f"### 问题 {i + 1}")
with gr.Row(equal_height=False):
with gr.Column(scale=3):
slot_components["question"] = gr.Markdown("问题文本...")
slot_components["options"] = gr.Markdown("- A. 选项A...")
slot_components["answer"] = gr.Markdown("**正确答案:** A")
slot_components["tables"] = gr.Markdown(visible=False)
with gr.Column(scale=2, min_width=250):
slot_components["single_img"] = gr.Image(show_label=False, height=250, interactive=False, visible=False)
slot_components["single_img_caption"] = gr.Markdown(visible=False, elem_classes=["caption"])
slot_components["gallery_img"] = gr.Gallery(label="问题图片", show_label=False, columns=1, object_fit="contain", height="auto", visible=False)
slot_components["gallery_img_caption"] = gr.Markdown(visible=False)
slot_components["critic_accordion"] = gr.Accordion(
label="🤖 评论智能体(o4-mini)【AI参考建议】",
open=True
)
with slot_components["critic_accordion"]:
slot_components["critic_text"] = gr.Markdown("评价详情...")
slot_components["reassessment_accordion"] = gr.Accordion(
"⚖️ 裁判智能体(o3)【AI参考建议】(如果信息不完整请查看'裁判智能体(o3)(总)')",
open=True
)
with slot_components["reassessment_accordion"]:
slot_components["reassessment_text"] = gr.Markdown("再评估详情...")
# 评论智能体部分
# gr.Markdown("#### **🤖 评论智能体(o4-mini)【AI参考建议】**")
# slot_components["critic_accordion"] = gr.Accordion("", open=True)
# with slot_components["critic_accordion"]:
# slot_components["critic_text"] = gr.Markdown("评价详情...")
# # 裁判智能体部分
# gr.Markdown("#### **⚖️ 裁判智能体(o3)【AI参考建议】** <small>*(如果信息不完整请查看'裁判智能体(o3)(总)')*</small>")
# slot_components["reassessment_accordion"] = gr.Accordion("", open=True)
# with slot_components["reassessment_accordion"]:
# slot_components["reassessment_text"] = gr.Markdown("再评估详情...")
# 新的双重验证系统
# 在Accordion之前添加一个Markdown标题
gr.Markdown("#### **🔍 审核与验证**") # 独立标题
slot_components["verification_accordion"] = gr.Accordion(
"",
open=True,
elem_classes=["verification-accordion"]
)
with slot_components["verification_accordion"]:
with gr.Column():
# 事实验证
gr.Markdown("#### 事实验证")
slot_components["factual_radio"] = gr.Radio(
label="事实验证正确或错误",
choices=["正确", "错误"],
value=None
)
slot_components["factual_comment"] = gr.Textbox(
label="指出事实错误,并依据【源报告】提出修改建议",
placeholder="请详细说明事实性错误的地方以及建议的修改...",
lines=2,
visible=False
)
gr.HTML("<hr style='margin: 10px 0;'>")
# 逻辑可解性
gr.Markdown("#### 逻辑可解性")
slot_components["logical_radio"] = gr.Radio(
label="逻辑可解性是或否",
choices=["是", "否"],
value=None
)
slot_components["logical_comment"] = gr.Textbox(
label="从【考生视角】说明逻辑不通之处及修改建议",
placeholder="请详细说明逻辑上的问题以及建议的修改...",
lines=2,
visible=False
)
# 添加到输入列表
verification_inputs_list.extend([
slot_components["factual_radio"],
slot_components["factual_comment"],
slot_components["logical_radio"],
slot_components["logical_comment"]
])
# 添加交互逻辑:条件性显示评论框
def create_factual_visibility_fn():
def update_factual_comment_visibility(choice):
return gr.update(visible=(choice == "错误"))
return update_factual_comment_visibility
def create_logical_visibility_fn():
def update_logical_comment_visibility(choice):
return gr.update(visible=(choice == "否"))
return update_logical_comment_visibility
slot_components["factual_radio"].change(
fn=create_factual_visibility_fn(),
inputs=[slot_components["factual_radio"]],
outputs=[slot_components["factual_comment"]]
)
slot_components["logical_radio"].change(
fn=create_logical_visibility_fn(),
inputs=[slot_components["logical_radio"]],
outputs=[slot_components["logical_comment"]]
)
gr.HTML("<hr style='border-top: 1px dashed #ccc; margin: 15px 0 5px 0;'>")
mcq_output_components.append(slot_components)
overall_reassessment_accordion = gr.Accordion("⚖️ 裁判智能体(o3)的总结【AI参考建议】", open=True, visible=False)
with overall_reassessment_accordion:
overall_reassessment_text = gr.Markdown("")
with gr.Row(equal_height=False, elem_classes=["nav-footer"]):
prev_btn = gr.Button("⬅️ 上一个文件", interactive=False)
next_btn = gr.Button("下一个文件 ➡️", interactive=False)
jump_to_file_input = gr.Number(label="跳转到文件 (序号)", minimum=1, step=1, precision=0, scale=1, interactive=False)
jump_to_file_btn = gr.Button("跳转", scale=0, interactive=False)
current_file_info_bottom = gr.Markdown("文件:0 / 0", elem_classes=["nav-info"])
save_btn = gr.Button("💾 保存所有结果")
save_status_md = gr.Markdown("", elem_classes=["save-status"])
# --- All updatable outputs ---
all_updatable_outputs = [
current_file_index_state, current_data_state, current_filepath_state, results_state,
status_message_md, case_display_area, title_md, level_info_md, ids_md, case_text_md,
discussion_text_md, general_tables_display_md, single_general_img,
single_general_caption_md, general_images_gallery_multi, general_images_captions_md, quality_assessment_md, scoring_summary_md,
scenario_text_md, single_scenario_img, single_scenario_caption_md,
scenario_images_gallery, scenario_images_captions_md, save_status_md,
current_file_info_bottom, prev_btn, next_btn, jump_to_file_input, jump_to_file_btn,
overall_reassessment_accordion, overall_reassessment_text
]
for slot in mcq_output_components:
all_updatable_outputs.extend([
slot["group"], slot["header"], slot["question"], slot["options"],
slot["answer"], slot["tables"], slot["single_img"], slot["single_img_caption"],
slot["gallery_img"], slot["gallery_img_caption"], slot["critic_accordion"],
slot["critic_text"], slot["reassessment_accordion"], slot["reassessment_text"],
slot["verification_accordion"], slot["factual_radio"], slot["factual_comment"],
slot["logical_radio"], slot["logical_comment"]
])
# --- Event Handlers ---
def handle_navigation(direction, current_idx, all_files, current_results, current_filepath, *verification_values):
if not all_files:
return (current_idx, current_data_state.value if hasattr(current_data_state, 'value') else {}, current_filepath, current_results) + tuple([gr.update()] * (len(all_updatable_outputs) - 4))
updated_results = collect_current_page_answers(current_filepath, current_results, *verification_values)
total_files = len(all_files)
new_idx = (current_idx + direction + total_files) % total_files
new_path = all_files[new_idx]
new_data, loaded_path = load_case_data_from_hf(new_path)
if loaded_path != new_path:
new_path = loaded_path
static_updates_dict = update_static_content(new_data, new_path)
mcq_updates_dict = update_mcq_components(new_data, new_path, updated_results)
nav_info_text = f"文件:{new_idx + 1} / {total_files} - {os.path.basename(new_path)}" if new_path and new_path != "INVALID_PATH" else "未加载文件"
nav_info_update_bottom = gr.update(value=nav_info_text)
combined_updates = [
new_idx, new_data, new_path, updated_results,
static_updates_dict.get("status_message_md", gr.update()),
static_updates_dict.get("case_display_area", gr.update()),
static_updates_dict.get("title_md", gr.update()),
static_updates_dict.get("level_info_md", gr.update()),
static_updates_dict.get("ids_md", gr.update()),
static_updates_dict.get("case_text_md", gr.update()),
static_updates_dict.get("discussion_text_md", gr.update()),
static_updates_dict.get("general_tables_display_md", gr.update()),
static_updates_dict.get("single_general_img", gr.update()),
static_updates_dict.get("single_general_caption_md", gr.update()),
static_updates_dict.get("general_images_gallery_multi", gr.update()),
static_updates_dict.get("general_images_captions_md", gr.update()),
static_updates_dict.get("quality_assessment_md", gr.update()),
static_updates_dict.get("scoring_summary_md", gr.update()),
static_updates_dict.get("scenario_text_md", gr.update()),
static_updates_dict.get("single_scenario_img", gr.update()),
static_updates_dict.get("single_scenario_caption_md", gr.update()),
static_updates_dict.get("scenario_images_gallery", gr.update()),
static_updates_dict.get("scenario_images_captions_md", gr.update()),
gr.update(value=""), nav_info_update_bottom,
gr.update(interactive=total_files > 1), gr.update(interactive=total_files > 1),
gr.update(maximum=total_files, interactive=total_files > 0), gr.update(interactive=total_files > 0),
static_updates_dict.get("overall_reassessment_accordion", gr.update()),
static_updates_dict.get("overall_reassessment_text", gr.update()),
]
for slot in mcq_output_components:
combined_updates.extend([
mcq_updates_dict.get(slot["group"], gr.update()),
mcq_updates_dict.get(slot["header"], gr.update()),
mcq_updates_dict.get(slot["question"], gr.update()),
mcq_updates_dict.get(slot["options"], gr.update()),
mcq_updates_dict.get(slot["answer"], gr.update()),
mcq_updates_dict.get(slot["tables"], gr.update()),
mcq_updates_dict.get(slot["single_img"], gr.update()),
mcq_updates_dict.get(slot["single_img_caption"], gr.update()),
mcq_updates_dict.get(slot["gallery_img"], gr.update()),
mcq_updates_dict.get(slot["gallery_img_caption"], gr.update()),
mcq_updates_dict.get(slot["critic_accordion"], gr.update()),
mcq_updates_dict.get(slot["critic_text"], gr.update()),
mcq_updates_dict.get(slot["reassessment_accordion"], gr.update()),
mcq_updates_dict.get(slot["reassessment_text"], gr.update()),
mcq_updates_dict.get(slot["verification_accordion"], gr.update()),
mcq_updates_dict.get(slot["factual_radio"], gr.update()),
mcq_updates_dict.get(slot["factual_comment"], gr.update()),
mcq_updates_dict.get(slot["logical_radio"], gr.update()),
mcq_updates_dict.get(slot["logical_comment"], gr.update())
])
return tuple(combined_updates)
def handle_jump_to_file(target_file_num_str, current_idx, all_files, current_data, current_results, current_filepath, *verification_values):
if not all_files or target_file_num_str is None:
return (current_idx, current_data, current_filepath, current_results) + tuple([gr.update()] * (len(all_updatable_outputs) - 4))
try:
target_file_num = int(float(target_file_num_str))
except (ValueError, TypeError):
error_message = "跳转失败:请输入有效的文件序号。"
return (current_idx, current_data, current_filepath, current_results, gr.update(value=error_message, visible=True)) + tuple([gr.update()] * (len(all_updatable_outputs) - 5))
total_files = len(all_files) if all_files else 0
if not (1 <= target_file_num <= total_files) and total_files > 0 :
error_message = f"跳转失败:文件序号必须在 1 到 {total_files} 之间。"
return (current_idx, current_data, current_filepath, current_results, gr.update(value=error_message, visible=True)) + tuple([gr.update()] * (len(all_updatable_outputs) - 5))
elif total_files == 0:
return (current_idx, current_data, current_filepath, current_results) + tuple([gr.update()] * (len(all_updatable_outputs) - 4))
updated_results = collect_current_page_answers(current_filepath, current_results, *verification_values)
new_idx = target_file_num - 1
new_path = all_files[new_idx]
new_data, loaded_path = load_case_data_from_hf(new_path)
if loaded_path != new_path:
new_path = loaded_path
static_updates_dict = update_static_content(new_data, new_path)
mcq_updates_dict = update_mcq_components(new_data, new_path, updated_results)
nav_info_text = f"文件:{new_idx + 1} / {total_files} - {os.path.basename(new_path)}" if new_path and new_path != "INVALID_PATH" else "未加载文件"
nav_info_update_bottom = gr.update(value=nav_info_text)
combined_updates = [
new_idx, new_data, new_path, updated_results,
static_updates_dict.get("status_message_md", gr.update()),
static_updates_dict.get("case_display_area", gr.update()),
static_updates_dict.get("title_md", gr.update()),
static_updates_dict.get("level_info_md", gr.update()),
static_updates_dict.get("ids_md", gr.update()),
static_updates_dict.get("case_text_md", gr.update()),
static_updates_dict.get("discussion_text_md", gr.update()),
static_updates_dict.get("general_tables_display_md", gr.update()),
static_updates_dict.get("single_general_img", gr.update()),
static_updates_dict.get("single_general_caption_md", gr.update()),
static_updates_dict.get("general_images_gallery_multi", gr.update()),
static_updates_dict.get("general_images_captions_md", gr.update()),
static_updates_dict.get("quality_assessment_md", gr.update()),
static_updates_dict.get("scoring_summary_md", gr.update()),
static_updates_dict.get("scenario_text_md", gr.update()),
static_updates_dict.get("single_scenario_img", gr.update()),
static_updates_dict.get("single_scenario_caption_md", gr.update()),
static_updates_dict.get("scenario_images_gallery", gr.update()),
static_updates_dict.get("scenario_images_captions_md", gr.update()),
gr.update(value=""), nav_info_update_bottom,
gr.update(interactive=total_files > 1), gr.update(interactive=total_files > 1),
gr.update(maximum=total_files, interactive=total_files > 0), gr.update(interactive=total_files > 0),
static_updates_dict.get("overall_reassessment_accordion", gr.update()),
static_updates_dict.get("overall_reassessment_text", gr.update())
]
for slot in mcq_output_components:
combined_updates.extend([
mcq_updates_dict.get(slot["group"], gr.update()),
mcq_updates_dict.get(slot["header"], gr.update()),
mcq_updates_dict.get(slot["question"], gr.update()),
mcq_updates_dict.get(slot["options"], gr.update()),
mcq_updates_dict.get(slot["answer"], gr.update()),
mcq_updates_dict.get(slot["tables"], gr.update()),
mcq_updates_dict.get(slot["single_img"], gr.update()),
mcq_updates_dict.get(slot["single_img_caption"], gr.update()),
mcq_updates_dict.get(slot["gallery_img"], gr.update()),
mcq_updates_dict.get(slot["gallery_img_caption"], gr.update()),
mcq_updates_dict.get(slot["critic_accordion"], gr.update()),
mcq_updates_dict.get(slot["critic_text"], gr.update()),
mcq_updates_dict.get(slot["reassessment_accordion"], gr.update()),
mcq_updates_dict.get(slot["reassessment_text"], gr.update()),
mcq_updates_dict.get(slot["verification_accordion"], gr.update()),
mcq_updates_dict.get(slot["factual_radio"], gr.update()),
mcq_updates_dict.get(slot["factual_comment"], gr.update()),
mcq_updates_dict.get(slot["logical_radio"], gr.update()),
mcq_updates_dict.get(slot["logical_comment"], gr.update())
])
return tuple(combined_updates)
def handle_save(current_filepath, current_results_state_val, *verification_values):
results_gathered_this_call = collect_current_page_answers(current_filepath, current_results_state_val, *verification_values)
message, data_intended_to_be_saved = save_results_to_hf(results_gathered_this_call)
return results_gathered_this_call, gr.update(value=message)
def populate_initial_ui_with_init():
"""首次加载时初始化并更新UI"""
# 初始化
all_files = find_json_files_from_hf()
results = load_results_from_hf()
if not all_files:
data = create_error_data("未找到数据文件")
path = "NO_FILES"
idx = -1
else:
idx = 0
path = all_files[0]
data, path = load_case_data_from_hf(path)
# 更新UI
static_updates_dict = update_static_content(data, path)
mcq_updates_dict = update_mcq_components(data, path, results)
total_files = len(all_files)
nav_info_text = f"文件:{idx + 1} / {total_files} - {os.path.basename(path)}" if idx >= 0 else "文件:0 / 0"
combined_updates = [
idx, data, path, results,
static_updates_dict.get("status_message_md", gr.update()),
static_updates_dict.get("case_display_area", gr.update()),
static_updates_dict.get("title_md", gr.update()),
static_updates_dict.get("level_info_md", gr.update()),
static_updates_dict.get("ids_md", gr.update()),
static_updates_dict.get("case_text_md", gr.update()),
static_updates_dict.get("discussion_text_md", gr.update()),
static_updates_dict.get("general_tables_display_md", gr.update()),
static_updates_dict.get("single_general_img", gr.update()),
static_updates_dict.get("single_general_caption_md", gr.update()),
static_updates_dict.get("general_images_gallery_multi", gr.update()),
static_updates_dict.get("general_images_captions_md", gr.update()),
static_updates_dict.get("quality_assessment_md", gr.update()),
static_updates_dict.get("scoring_summary_md", gr.update()),
static_updates_dict.get("scenario_text_md", gr.update()),
static_updates_dict.get("single_scenario_img", gr.update()),
static_updates_dict.get("single_scenario_caption_md", gr.update()),
static_updates_dict.get("scenario_images_gallery", gr.update()),
static_updates_dict.get("scenario_images_captions_md", gr.update()),
gr.update(value=""), gr.update(value=nav_info_text),
gr.update(interactive=total_files > 1), gr.update(interactive=total_files > 1),
gr.update(maximum=total_files if total_files > 0 else 1, interactive=total_files > 0),
gr.update(interactive=total_files > 0),
static_updates_dict.get("overall_reassessment_accordion", gr.update()),
static_updates_dict.get("overall_reassessment_text", gr.update())
]
for slot in mcq_output_components:
combined_updates.extend([
mcq_updates_dict.get(slot["group"], gr.update()),
mcq_updates_dict.get(slot["header"], gr.update()),
mcq_updates_dict.get(slot["question"], gr.update()),
mcq_updates_dict.get(slot["options"], gr.update()),
mcq_updates_dict.get(slot["answer"], gr.update()),
mcq_updates_dict.get(slot["tables"], gr.update()),
mcq_updates_dict.get(slot["single_img"], gr.update()),
mcq_updates_dict.get(slot["single_img_caption"], gr.update()),
mcq_updates_dict.get(slot["gallery_img"], gr.update()),
mcq_updates_dict.get(slot["gallery_img_caption"], gr.update()),
mcq_updates_dict.get(slot["critic_accordion"], gr.update()),
mcq_updates_dict.get(slot["critic_text"], gr.update()),
mcq_updates_dict.get(slot["reassessment_accordion"], gr.update()),
mcq_updates_dict.get(slot["reassessment_text"], gr.update()),
mcq_updates_dict.get(slot["verification_accordion"], gr.update()),
mcq_updates_dict.get(slot["factual_radio"], gr.update()),
mcq_updates_dict.get(slot["factual_comment"], gr.update()),
mcq_updates_dict.get(slot["logical_radio"], gr.update()),
mcq_updates_dict.get(slot["logical_comment"], gr.update())
])
# 更新状态
all_json_files_state.value = all_files
return tuple(combined_updates)
navigation_inputs = [current_file_index_state, all_json_files_state, results_state, current_filepath_state] + verification_inputs_list
jump_inputs = [jump_to_file_input, current_file_index_state, all_json_files_state, current_data_state, results_state, current_filepath_state] + verification_inputs_list
save_inputs = [current_filepath_state, results_state] + verification_inputs_list
next_btn.click(fn=handle_navigation, inputs=[gr.State(value=1)] + navigation_inputs, outputs=all_updatable_outputs, queue=False)
prev_btn.click(fn=handle_navigation, inputs=[gr.State(value=-1)] + navigation_inputs, outputs=all_updatable_outputs, queue=False)
save_btn.click(fn=handle_save, inputs=save_inputs, outputs=[results_state, save_status_md])
jump_to_file_btn.click(fn=handle_jump_to_file, inputs=jump_inputs, outputs=all_updatable_outputs, queue=False)
app.load(
fn=populate_initial_ui_with_init,
inputs=None,
outputs=all_updatable_outputs
)
# --- Launch ---
if __name__ == "__main__":
print("准备启动Gradio应用...")
# 从环境变量获取认证信息
gradio_username = os.environ.get(GRADIO_USERNAME_ENV_VAR)
gradio_password = os.environ.get(GRADIO_PASSWORD_ENV_VAR)
auth = None
if gradio_username and gradio_password:
auth = (gradio_username, gradio_password)
print("Gradio 认证已启用")
else:
print("Warning: 未找到Gradio认证环境变量,应用将在无保护模式下运行")
app.launch(show_error=True, auth=auth)