app.py
CHANGED
|
@@ -8,10 +8,10 @@ class ReviewState:
|
|
| 8 |
def __init__(self):
|
| 9 |
self.all_paths = []
|
| 10 |
self.current_idx = -1
|
| 11 |
-
|
| 12 |
def sync_paths(self):
|
| 13 |
self.all_paths = data_manager.get_all_chart_paths()
|
| 14 |
-
|
| 15 |
def get_nav_target(self, direction):
|
| 16 |
new_idx = self.current_idx + direction
|
| 17 |
if 0 <= new_idx < len(self.all_paths):
|
|
@@ -32,7 +32,6 @@ def to_html_frame(html_content):
|
|
| 32 |
def handle_source_change(source):
|
| 33 |
struct = data_manager.get_dataset_structure()
|
| 34 |
types = list(struct.get('sources', {}).get(source, {}).get('chart_types', {}).keys())
|
| 35 |
-
# 默认选中第一个
|
| 36 |
return gr.update(choices=types, value=types[0] if types else None)
|
| 37 |
|
| 38 |
def handle_type_change(source, c_type):
|
|
@@ -46,29 +45,25 @@ def handle_type_change(source, c_type):
|
|
| 46 |
|
| 47 |
def handle_load(source, c_type, c_id, model):
|
| 48 |
if not all([source, c_type, c_id, model]):
|
| 49 |
-
# 必须返回 8 个 update 对象以匹配 outputs
|
| 50 |
return [gr.update()] * 8
|
| 51 |
-
|
| 52 |
-
# 获取数据
|
| 53 |
chart_data = data_manager.get_chart_data(source, c_type, c_id)
|
| 54 |
qa_list = data_manager.get_qa_list(source, c_type, model, c_id)
|
| 55 |
stats = data_manager.get_review_stats()
|
| 56 |
-
|
| 57 |
-
# 同步索引位置
|
| 58 |
nav_state.sync_paths()
|
| 59 |
for i, p in enumerate(nav_state.all_paths):
|
| 60 |
if p['chart_id'] == c_id and p['model'] == model:
|
| 61 |
nav_state.current_idx = i
|
| 62 |
break
|
| 63 |
|
| 64 |
-
# 准备 UI 数据
|
| 65 |
html_code = to_html_frame(chart_data.get('html_content', ''))
|
| 66 |
meta_md = "\n".join([f"- **{k}**: {v}" for k, v in chart_data.get('label_info', {}).items()])
|
| 67 |
qa_json = json.dumps([{"id": q.id, "q": q.question, "a": q.answer} for q in qa_list])
|
| 68 |
stats_str = f"✅{stats['correct']} | ❌{stats['incorrect']} | 总{stats['total']}"
|
| 69 |
prog_str = f"{nav_state.current_idx + 1} / {len(nav_state.all_paths)}"
|
| 70 |
radio_choices = [f"Q{i+1}: {q.question[:20]}..." for i, q in enumerate(qa_list)]
|
| 71 |
-
|
| 72 |
return [
|
| 73 |
html_code,
|
| 74 |
meta_md,
|
|
@@ -77,7 +72,7 @@ def handle_load(source, c_type, c_id, model):
|
|
| 77 |
prog_str,
|
| 78 |
f"{source}/{c_type}/{c_id}",
|
| 79 |
gr.update(choices=radio_choices, value=radio_choices[0] if radio_choices else None),
|
| 80 |
-
json.dumps({})
|
| 81 |
]
|
| 82 |
|
| 83 |
def handle_qa_select(selection, qa_json):
|
|
@@ -94,7 +89,6 @@ def handle_qa_select(selection, qa_json):
|
|
| 94 |
# ============== UI 布局 ==============
|
| 95 |
def create_ui():
|
| 96 |
with gr.Blocks(title="审核系统 V2", theme=gr.themes.Soft()) as demo:
|
| 97 |
-
# 显式初始化 State,避免布尔值陷阱
|
| 98 |
qa_store = gr.State(value="[]")
|
| 99 |
review_store = gr.State(value="{}")
|
| 100 |
|
|
@@ -118,7 +112,7 @@ def create_ui():
|
|
| 118 |
|
| 119 |
with gr.Accordion("元数据解析", open=False):
|
| 120 |
meta_md = gr.Markdown()
|
| 121 |
-
|
| 122 |
gr.Markdown("---")
|
| 123 |
qa_radio = gr.Radio(label="题目列表", choices=[])
|
| 124 |
|
|
@@ -126,39 +120,43 @@ def create_ui():
|
|
| 126 |
curr_qid = gr.Text(visible=False)
|
| 127 |
q_disp = gr.Text(label="问题内容", lines=2)
|
| 128 |
a_disp = gr.Text(label="标准答案")
|
| 129 |
-
|
| 130 |
status_opt = gr.Radio(
|
| 131 |
label="审核结论",
|
| 132 |
-
|
|
|
|
| 133 |
value="correct"
|
| 134 |
)
|
| 135 |
err_type = gr.Dropdown(label="错误分类", choices=["无", "事实错误", "逻辑错误", "图表无法读取"])
|
| 136 |
-
|
| 137 |
comment = gr.Text(label="审核备注")
|
|
|
|
|
|
|
|
|
|
| 138 |
save_btn = gr.Button("💾 提交单条审核", variant="primary")
|
| 139 |
|
| 140 |
with gr.Row():
|
| 141 |
prev_btn = gr.Button("⬅️ 上一个图表")
|
| 142 |
next_btn = gr.Button("➡️ 下一个图表")
|
| 143 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
# --- 事件绑定 ---
|
| 145 |
-
|
| 146 |
-
# 初始化第一级
|
| 147 |
demo.load(
|
| 148 |
fn=lambda: gr.update(choices=list(data_manager.get_dataset_structure().get('sources', {}).keys())),
|
| 149 |
outputs=[src_dd]
|
| 150 |
)
|
| 151 |
|
| 152 |
-
# 联动逻辑
|
| 153 |
src_dd.change(handle_source_change, inputs=[src_dd], outputs=[typ_dd])
|
| 154 |
typ_dd.change(handle_type_change, inputs=[src_dd, typ_dd], outputs=[id_dd, mdl_dd])
|
| 155 |
-
|
| 156 |
-
# 加载数据 (严格匹配 8 个 output)
|
| 157 |
load_event_outputs = [chart_view, meta_md, qa_store, stats_txt, prog_txt, path_info, qa_radio, review_store]
|
| 158 |
id_dd.change(handle_load, inputs=[src_dd, typ_dd, id_dd, mdl_dd], outputs=load_event_outputs)
|
| 159 |
mdl_dd.change(handle_load, inputs=[src_dd, typ_dd, id_dd, mdl_dd], outputs=load_event_outputs)
|
| 160 |
|
| 161 |
-
# 题目切换
|
| 162 |
qa_radio.change(
|
| 163 |
handle_qa_select,
|
| 164 |
inputs=[qa_radio, qa_store],
|
|
@@ -173,36 +171,55 @@ def create_ui():
|
|
| 173 |
gr.update(value=target['source']),
|
| 174 |
gr.update(value=target['chart_type']),
|
| 175 |
gr.update(value=target['chart_id']),
|
| 176 |
-
gr.update(value=target['model'])
|
|
|
|
| 177 |
]
|
| 178 |
-
return [gr.update()] * 4
|
|
|
|
|
|
|
|
|
|
| 179 |
|
| 180 |
-
|
| 181 |
-
next_btn.click(lambda: navigate(1), outputs=[src_dd, typ_dd, id_dd, mdl_dd])
|
| 182 |
-
|
| 183 |
-
# 保存逻辑
|
| 184 |
def quick_save(qid, cid, src, status, cmt):
|
| 185 |
-
if not qid:
|
|
|
|
|
|
|
| 186 |
data_manager.save_review({
|
| 187 |
"qa_id": qid, "chart_id": cid, "source": src,
|
| 188 |
"status": status, "comment": cmt
|
| 189 |
})
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
save_btn.click(
|
| 193 |
quick_save,
|
| 194 |
inputs=[curr_qid, id_dd, src_dd, status_opt, comment],
|
| 195 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
)
|
| 197 |
|
| 198 |
return demo
|
| 199 |
|
| 200 |
if __name__ == "__main__":
|
| 201 |
app = create_ui()
|
| 202 |
-
# 强制禁用所有 API 预览功能
|
| 203 |
app.launch(
|
| 204 |
server_name="0.0.0.0",
|
| 205 |
server_port=7860,
|
| 206 |
show_api=False,
|
| 207 |
max_threads=10
|
| 208 |
-
)
|
|
|
|
| 8 |
def __init__(self):
|
| 9 |
self.all_paths = []
|
| 10 |
self.current_idx = -1
|
| 11 |
+
|
| 12 |
def sync_paths(self):
|
| 13 |
self.all_paths = data_manager.get_all_chart_paths()
|
| 14 |
+
|
| 15 |
def get_nav_target(self, direction):
|
| 16 |
new_idx = self.current_idx + direction
|
| 17 |
if 0 <= new_idx < len(self.all_paths):
|
|
|
|
| 32 |
def handle_source_change(source):
|
| 33 |
struct = data_manager.get_dataset_structure()
|
| 34 |
types = list(struct.get('sources', {}).get(source, {}).get('chart_types', {}).keys())
|
|
|
|
| 35 |
return gr.update(choices=types, value=types[0] if types else None)
|
| 36 |
|
| 37 |
def handle_type_change(source, c_type):
|
|
|
|
| 45 |
|
| 46 |
def handle_load(source, c_type, c_id, model):
|
| 47 |
if not all([source, c_type, c_id, model]):
|
|
|
|
| 48 |
return [gr.update()] * 8
|
| 49 |
+
|
|
|
|
| 50 |
chart_data = data_manager.get_chart_data(source, c_type, c_id)
|
| 51 |
qa_list = data_manager.get_qa_list(source, c_type, model, c_id)
|
| 52 |
stats = data_manager.get_review_stats()
|
| 53 |
+
|
|
|
|
| 54 |
nav_state.sync_paths()
|
| 55 |
for i, p in enumerate(nav_state.all_paths):
|
| 56 |
if p['chart_id'] == c_id and p['model'] == model:
|
| 57 |
nav_state.current_idx = i
|
| 58 |
break
|
| 59 |
|
|
|
|
| 60 |
html_code = to_html_frame(chart_data.get('html_content', ''))
|
| 61 |
meta_md = "\n".join([f"- **{k}**: {v}" for k, v in chart_data.get('label_info', {}).items()])
|
| 62 |
qa_json = json.dumps([{"id": q.id, "q": q.question, "a": q.answer} for q in qa_list])
|
| 63 |
stats_str = f"✅{stats['correct']} | ❌{stats['incorrect']} | 总{stats['total']}"
|
| 64 |
prog_str = f"{nav_state.current_idx + 1} / {len(nav_state.all_paths)}"
|
| 65 |
radio_choices = [f"Q{i+1}: {q.question[:20]}..." for i, q in enumerate(qa_list)]
|
| 66 |
+
|
| 67 |
return [
|
| 68 |
html_code,
|
| 69 |
meta_md,
|
|
|
|
| 72 |
prog_str,
|
| 73 |
f"{source}/{c_type}/{c_id}",
|
| 74 |
gr.update(choices=radio_choices, value=radio_choices[0] if radio_choices else None),
|
| 75 |
+
json.dumps({})
|
| 76 |
]
|
| 77 |
|
| 78 |
def handle_qa_select(selection, qa_json):
|
|
|
|
| 89 |
# ============== UI 布局 ==============
|
| 90 |
def create_ui():
|
| 91 |
with gr.Blocks(title="审核系统 V2", theme=gr.themes.Soft()) as demo:
|
|
|
|
| 92 |
qa_store = gr.State(value="[]")
|
| 93 |
review_store = gr.State(value="{}")
|
| 94 |
|
|
|
|
| 112 |
|
| 113 |
with gr.Accordion("元数据解析", open=False):
|
| 114 |
meta_md = gr.Markdown()
|
| 115 |
+
|
| 116 |
gr.Markdown("---")
|
| 117 |
qa_radio = gr.Radio(label="题目列表", choices=[])
|
| 118 |
|
|
|
|
| 120 |
curr_qid = gr.Text(visible=False)
|
| 121 |
q_disp = gr.Text(label="问题内容", lines=2)
|
| 122 |
a_disp = gr.Text(label="标准答案")
|
|
|
|
| 123 |
status_opt = gr.Radio(
|
| 124 |
label="审核结论",
|
| 125 |
+
# 【修正】将 "modified" 改为 "needs_modification" 匹配 DataManager 内部逻辑
|
| 126 |
+
choices=[("正确", "correct"), ("错误", "incorrect"), ("优化", "needs_modification")],
|
| 127 |
value="correct"
|
| 128 |
)
|
| 129 |
err_type = gr.Dropdown(label="错误分类", choices=["无", "事实错误", "逻辑错误", "图表无法读取"])
|
| 130 |
+
|
| 131 |
comment = gr.Text(label="审核备注")
|
| 132 |
+
|
| 133 |
+
# 【新增】状态提示框,用于反馈保存成功与否
|
| 134 |
+
save_status = gr.Text(label="操作反馈", interactive=False)
|
| 135 |
save_btn = gr.Button("💾 提交单条审核", variant="primary")
|
| 136 |
|
| 137 |
with gr.Row():
|
| 138 |
prev_btn = gr.Button("⬅️ 上一个图表")
|
| 139 |
next_btn = gr.Button("➡️ 下一个图表")
|
| 140 |
|
| 141 |
+
gr.Markdown("---")
|
| 142 |
+
# 【新增】下载区
|
| 143 |
+
with gr.Row():
|
| 144 |
+
export_btn = gr.Button("📥 导出并下载审核记录", variant="secondary")
|
| 145 |
+
download_file = gr.File(label="下载文件", visible=False)
|
| 146 |
+
|
| 147 |
# --- 事件绑定 ---
|
|
|
|
|
|
|
| 148 |
demo.load(
|
| 149 |
fn=lambda: gr.update(choices=list(data_manager.get_dataset_structure().get('sources', {}).keys())),
|
| 150 |
outputs=[src_dd]
|
| 151 |
)
|
| 152 |
|
|
|
|
| 153 |
src_dd.change(handle_source_change, inputs=[src_dd], outputs=[typ_dd])
|
| 154 |
typ_dd.change(handle_type_change, inputs=[src_dd, typ_dd], outputs=[id_dd, mdl_dd])
|
| 155 |
+
|
|
|
|
| 156 |
load_event_outputs = [chart_view, meta_md, qa_store, stats_txt, prog_txt, path_info, qa_radio, review_store]
|
| 157 |
id_dd.change(handle_load, inputs=[src_dd, typ_dd, id_dd, mdl_dd], outputs=load_event_outputs)
|
| 158 |
mdl_dd.change(handle_load, inputs=[src_dd, typ_dd, id_dd, mdl_dd], outputs=load_event_outputs)
|
| 159 |
|
|
|
|
| 160 |
qa_radio.change(
|
| 161 |
handle_qa_select,
|
| 162 |
inputs=[qa_radio, qa_store],
|
|
|
|
| 171 |
gr.update(value=target['source']),
|
| 172 |
gr.update(value=target['chart_type']),
|
| 173 |
gr.update(value=target['chart_id']),
|
| 174 |
+
gr.update(value=target['model']),
|
| 175 |
+
gr.update(value="") # 切换时清空状态
|
| 176 |
]
|
| 177 |
+
return [gr.update()] * 4 + [gr.update()]
|
| 178 |
+
|
| 179 |
+
prev_btn.click(lambda: navigate(-1), outputs=[src_dd, typ_dd, id_dd, mdl_dd, save_status])
|
| 180 |
+
next_btn.click(lambda: navigate(1), outputs=[src_dd, typ_dd, id_dd, mdl_dd, save_status])
|
| 181 |
|
| 182 |
+
# 【修复与增强】保存逻辑
|
|
|
|
|
|
|
|
|
|
| 183 |
def quick_save(qid, cid, src, status, cmt):
|
| 184 |
+
if not qid:
|
| 185 |
+
return "⚠️ 无效操作:未选择题目", gr.update()
|
| 186 |
+
|
| 187 |
data_manager.save_review({
|
| 188 |
"qa_id": qid, "chart_id": cid, "source": src,
|
| 189 |
"status": status, "comment": cmt
|
| 190 |
})
|
| 191 |
+
|
| 192 |
+
# 【增强】保存后立刻刷新全局统计信息
|
| 193 |
+
stats = data_manager.get_review_stats()
|
| 194 |
+
stats_str = f"✅{stats['correct']} | ❌{stats['incorrect']} | 总{stats['total']}"
|
| 195 |
+
|
| 196 |
+
return f"✅ 保存成功 (ID: {qid})", stats_str
|
| 197 |
|
| 198 |
save_btn.click(
|
| 199 |
quick_save,
|
| 200 |
inputs=[curr_qid, id_dd, src_dd, status_opt, comment],
|
| 201 |
+
# 【修复】将结果输出到已经实例化的组件上
|
| 202 |
+
outputs=[save_status, stats_txt]
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
# 【新增】导出下载逻辑
|
| 206 |
+
def handle_export():
|
| 207 |
+
file_path = data_manager.export_reviews()
|
| 208 |
+
return gr.update(value=file_path, visible=True)
|
| 209 |
+
|
| 210 |
+
export_btn.click(
|
| 211 |
+
handle_export,
|
| 212 |
+
inputs=[],
|
| 213 |
+
outputs=[download_file]
|
| 214 |
)
|
| 215 |
|
| 216 |
return demo
|
| 217 |
|
| 218 |
if __name__ == "__main__":
|
| 219 |
app = create_ui()
|
|
|
|
| 220 |
app.launch(
|
| 221 |
server_name="0.0.0.0",
|
| 222 |
server_port=7860,
|
| 223 |
show_api=False,
|
| 224 |
max_threads=10
|
| 225 |
+
)
|