adddrett commited on
Commit
afaa666
·
1 Parent(s): 8dd9716
Files changed (1) hide show
  1. app.py +174 -307
app.py CHANGED
@@ -1,341 +1,208 @@
1
- """
2
- 图表问答数据集审核系统 - Gradio 5.x 应用
3
- """
4
  import gradio as gr
5
- from data_manager import DataManager, data_manager
6
- from typing import Dict, List, Optional, Tuple, Any
7
  import json
8
- import os
9
  import base64
10
- import tempfile
11
 
12
- # ============== 全局状态 ==============
13
-
14
- class AppState:
15
  def __init__(self):
16
- self.current_source: str = ""
17
- self.current_chart_type: str = ""
18
- self.current_chart_id: str = ""
19
- self.current_model: str = ""
20
- self.all_paths: List[Dict] = []
21
- self.current_index: int = -1
22
- self.refresh_paths()
23
 
24
- def refresh_paths(self):
25
  self.all_paths = data_manager.get_all_chart_paths()
26
 
27
- def get_current_path(self) -> Optional[Dict]:
28
- if 0 <= self.current_index < len(self.all_paths):
29
- return self.all_paths[self.current_index]
 
 
30
  return None
31
-
32
- def set_position(self, source: str, chart_type: str, chart_id: str, model: str):
33
- self.current_source = source
34
- self.current_chart_type = chart_type
35
- self.current_chart_id = chart_id
36
- self.current_model = model
37
- for i, path in enumerate(self.all_paths):
38
- if (path['source'] == source and
39
- path['chart_type'] == chart_type and
40
- path['chart_id'] == chart_id and
41
- path['model'] == model):
42
- self.current_index = i
43
- break
44
-
45
- def navigate(self, direction: int) -> bool:
46
- new_index = self.current_index + direction
47
- if 0 <= new_index < len(self.all_paths):
48
- self.current_index = new_index
49
- path = self.all_paths[new_index]
50
- self.current_source = path['source']
51
- self.current_chart_type = path['chart_type']
52
- self.current_chart_id = path['chart_id']
53
- self.current_model = path['model']
54
- return True
55
- return False
56
-
57
- state = AppState()
58
 
59
- # 当前 QA 数据缓存(使用简单全局变量,避免 gr.State 的问题)
60
- _current_qa_list: List = []
61
- _current_reviews: Dict = {}
62
 
63
- # ============== UI 更新函数 ==============
64
-
65
- def init_dataset():
66
- structure = data_manager.get_dataset_structure()
67
- sources = list(structure.get('sources', {}).keys())
68
- return gr.Dropdown(choices=sources, value=sources[0] if sources else None)
69
-
70
- def update_chart_type_dropdown(source: str):
71
- state.current_source = source
72
- structure = data_manager.get_dataset_structure()
73
- chart_types = list(structure.get('sources', {}).get(source, {}).get('chart_types', {}).keys())
74
- return gr.Dropdown(choices=chart_types, value=chart_types[0] if chart_types else None)
75
-
76
- def update_chart_dropdown(source: str, chart_type: str):
77
- state.current_source = source
78
- state.current_chart_type = chart_type
79
- charts = data_manager.get_chart_list(source, chart_type)
80
- structure = data_manager.get_dataset_structure()
81
- ct_data = structure.get('sources', {}).get(source, {}).get('chart_types', {}).get(chart_type, {})
82
- models = ct_data.get('models', [])
83
- return (
84
- gr.Dropdown(choices=charts, value=charts[0] if charts else None),
85
- gr.Dropdown(choices=models, value=models[0] if models else None)
86
- )
87
-
88
- def create_embedded_html(html_content: str, chart_id: str = "") -> str:
89
  if not html_content:
90
- return f'''<div style="display:flex;flex-direction:column;min-height:750px;align-items:center;justify-content:center;background:#fafafa;border:2px dashed #ddd;border-radius:12px;">
91
- <div style="font-size:48px;margin-bottom:16px;">📭</div>
92
- <div style="font-size:18px;color:#666;">暂无图表内容</div>
93
- <div style="font-size:14px;color:#999;margin-top:8px;">ID: {chart_id}</div>
94
- </div>'''
95
- html_base64 = base64.b64encode(html_content.encode('utf-8')).decode('utf-8')
96
- return f'<iframe src="data:text/html;base64,{html_base64}" style="width:100%;height:750px;border:1px solid #e0e0e0;border-radius:8px;background:#fff;" sandbox="allow-scripts allow-same-origin"></iframe>'
97
-
98
- def load_chart_data(source: str, chart_type: str, chart_id: str, model: str):
99
- global _current_qa_list, _current_reviews
100
-
101
- if not all([source, chart_type, chart_id, model]):
102
- _current_qa_list = []
103
- _current_reviews = {}
104
- return (
105
- create_embedded_html(""),
106
- "### 请选择图表",
107
- "等待加载...",
108
- "请选择图表",
109
- gr.Radio(choices=[], value=None),
110
- ""
111
- )
112
-
113
- state.set_position(source, chart_type, chart_id, model)
114
- chart_data = data_manager.get_chart_data(source, chart_type, chart_id)
115
- html_content = chart_data.get('html_content', '')
116
- label_info = chart_data.get('label_info', {})
117
-
118
- embedded_html = create_embedded_html(html_content, chart_id)
119
-
120
- # 格式化标签
121
- if label_info:
122
- label_lines = ["### 📝 图表信息", ""]
123
- for k, v in label_info.items():
124
- label_lines.append(f"**{k}**: {v}")
125
- label_text = "\n".join(label_lines)
126
- else:
127
- label_text = "### ⚠️ 暂无标签信息"
128
-
129
- # 获取 QA 数据
130
- qa_list = data_manager.get_qa_list(source, chart_type, model, chart_id)
131
- _current_qa_list = [{"id": qa.id, "question": qa.question, "answer": qa.answer} for qa in qa_list]
132
-
133
- # 获取审核记录
134
- reviews = data_manager.get_reviews_by_chart(chart_id, model)
135
- _current_reviews = {r['qa_id']: r for r in reviews}
136
-
137
- # 统计
138
- stats = data_manager.get_review_stats()
139
- status_msg = f"已审核: {stats['total']} | ✅正确: {stats['correct']} | ❌错误: {stats['incorrect']} | ✏️需修改: {stats['needs_modification']}"
140
- progress_msg = f"进度: {state.current_index + 1} / {len(state.all_paths)}"
141
-
142
- qa_choices = [f"Q{i+1}: {qa.question[:40]}..." for i, qa in enumerate(qa_list)]
143
-
144
- debug_info = f"📁 {source}/{chart_type}/{chart_id} | HTML: {len(html_content)} 字符"
145
-
146
  return (
147
- embedded_html,
148
- label_text,
149
- status_msg,
150
- progress_msg,
151
- gr.Radio(choices=qa_choices, value=qa_choices[0] if qa_choices else None),
152
- debug_info
153
  )
154
 
155
- def on_qa_select(qa_index_str: str):
156
- """选择 QA 后更新审核面板"""
157
- global _current_qa_list, _current_reviews
 
158
 
159
- if not qa_index_str or not _current_qa_list:
160
- return ("", "", "", "correct", "", "", "", "")
 
 
161
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  try:
163
- idx = int(qa_index_str.split(":")[0].replace("Q", "")) - 1
164
- qa = _current_qa_list[idx]
165
- review = _current_reviews.get(qa['id'], {})
166
-
167
- return (
168
- qa['id'],
169
- qa['question'],
170
- qa['answer'],
171
- review.get('status', 'correct'),
172
- review.get('issue_type', ''),
173
- review.get('modified_question', ''),
174
- review.get('modified_answer', ''),
175
- review.get('comment', '')
176
- )
177
- except Exception as e:
178
- print(f"Error: {e}")
179
- return ("", "", "", "correct", "", "", "", "")
180
-
181
- def navigate_prev():
182
- if state.navigate(-1):
183
- path = state.get_current_path()
184
- if path:
185
- return (
186
- gr.Dropdown(value=path['source']),
187
- gr.Dropdown(value=path['chart_type']),
188
- gr.Dropdown(value=path['chart_id']),
189
- gr.Dropdown(value=path['model'])
190
- )
191
- return (gr.Dropdown(), gr.Dropdown(), gr.Dropdown(), gr.Dropdown())
192
-
193
- def navigate_next():
194
- if state.navigate(1):
195
- path = state.get_current_path()
196
- if path:
197
- return (
198
- gr.Dropdown(value=path['source']),
199
- gr.Dropdown(value=path['chart_type']),
200
- gr.Dropdown(value=path['chart_id']),
201
- gr.Dropdown(value=path['model'])
202
- )
203
- return (gr.Dropdown(), gr.Dropdown(), gr.Dropdown(), gr.Dropdown())
204
-
205
- def save_review_handler(qa_id, chart_id, source, chart_type, model, orig_q, orig_a, status, mod_q, mod_a, issue_type, comment, reviewer):
206
- if not qa_id:
207
- return "❌ 请先选择问答对"
208
-
209
- data_manager.save_review({
210
- "qa_id": qa_id,
211
- "chart_id": chart_id,
212
- "source": source,
213
- "chart_type": chart_type,
214
- "model": model,
215
- "original_question": orig_q,
216
- "original_answer": orig_a,
217
- "status": status,
218
- "modified_question": mod_q,
219
- "modified_answer": mod_a,
220
- "issue_type": issue_type,
221
- "comment": comment,
222
- "reviewer": reviewer
223
- })
224
-
225
- # 更新缓存
226
- global _current_reviews
227
- _current_reviews[qa_id] = {
228
- "qa_id": qa_id, "status": status, "issue_type": issue_type,
229
- "modified_question": mod_q, "modified_answer": mod_a, "comment": comment
230
- }
231
-
232
- stats = data_manager.get_review_stats()
233
- return f"✅ 已保存! 总计: {stats['total']} | ✅正确: {stats['correct']} | ❌错误: {stats['incorrect']}"
234
-
235
- def export_reviews_handler():
236
- reviews = data_manager.get_all_reviews()
237
- if not reviews:
238
- return None
239
- temp_path = os.path.join(tempfile.gettempdir(), "reviews_export.json")
240
- with open(temp_path, 'w', encoding='utf-8') as f:
241
- json.dump(reviews, f, ensure_ascii=False, indent=2)
242
- return temp_path
243
-
244
- # ============== 创建 Gradio 界面 ==============
245
-
246
  def create_ui():
247
- custom_css = """
248
- .chart-container { min-height: 780px; }
249
- .control-panel { background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 8px; }
250
- """
251
-
252
- with gr.Blocks(title="图表问答数据集审核系统", theme=gr.themes.Soft(), css=custom_css) as app:
253
- gr.Markdown("# 📊 图表问答数据集审核系统\n\n审核图表问答对,使用 ← → 按钮切换图表")
254
 
255
- # 顶部状态
256
- with gr.Row():
257
- status_text = gr.Textbox(label="统计", interactive=False, scale=2)
258
- progress_text = gr.Textbox(label="进度", interactive=False, scale=1)
259
-
260
- # 导航栏
261
- with gr.Row():
262
- source_drop = gr.Dropdown(label="数据来源", choices=[], interactive=True, scale=1)
263
- type_drop = gr.Dropdown(label="图表类型", choices=[], interactive=True, scale=1)
264
- id_drop = gr.Dropdown(label="图表ID", choices=[], interactive=True, scale=1)
265
- model_drop = gr.Dropdown(label="模型", choices=[], interactive=True, scale=1)
266
- reviewer = gr.Textbox(label="审核人", value="admin", interactive=True, scale=1)
267
 
268
  with gr.Row():
269
- prev_btn = gr.Button("⬅️ 上一个", size="sm")
270
- next_btn = gr.Button("➡️ 下一个", size="sm")
271
- export_btn = gr.Button("📦 导出记录", variant="secondary", size="sm")
272
- download_file = gr.File(label="下载", visible=False)
273
-
274
- # 主内容
275
- with gr.Row():
276
- # 左侧:图表
277
- with gr.Column(scale=6):
278
- html_display = gr.HTML(elem_classes=["chart-container"])
279
- debug_info = gr.Textbox(label="调试", interactive=False, show_label=False)
280
-
281
- # 右侧:审核
282
  with gr.Column(scale=4):
283
- with gr.Accordion("📝 图表元数据", open=False):
284
- label_display = gr.Markdown()
 
 
 
285
 
286
- gr.Markdown("### ❓ 问答审核")
287
- qa_selector = gr.Radio(label="选择问答对", choices=[], interactive=True)
288
 
289
- current_qa_id = gr.Textbox(visible=False, value="")
290
- orig_q = gr.Textbox(label="原始问题", interactive=False, lines=2)
291
- orig_a = gr.Textbox(label="原始答案", interactive=False)
 
292
 
 
 
 
293
  gr.Markdown("---")
294
- status_radio = gr.Radio(
295
- label="判定结果",
296
- choices=[("✅ 正确", "correct"), ("❌ 错误", "incorrect"), ("✏️ 需修改", "needs_modification")],
297
- value="correct",
298
- interactive=True
299
- )
300
- issue_type = gr.Dropdown(
301
- label="错误类型",
302
- choices=["答案错误", "问题模糊", "图文不符", "其他"],
303
- interactive=True
304
- )
305
- mod_q = gr.Textbox(label="修改后问题", lines=2, interactive=True)
306
- mod_a = gr.Textbox(label="修改后答案", interactive=True)
307
- comment = gr.Textbox(label="备注", lines=2, interactive=True)
308
 
309
- save_btn = gr.Button("💾 保存审核", variant="primary")
310
- save_msg = gr.Textbox(label="", visible=False)
311
-
312
- # ===== 事件绑定 =====
313
- app.load(fn=init_dataset, outputs=[source_drop])
314
-
315
- source_drop.change(fn=update_chart_type_dropdown, inputs=[source_drop], outputs=[type_drop])
316
- type_drop.change(fn=update_chart_dropdown, inputs=[source_drop, type_drop], outputs=[id_drop, model_drop])
317
-
318
- load_outputs = [html_display, label_display, status_text, progress_text, qa_selector, debug_info]
319
- id_drop.change(fn=load_chart_data, inputs=[source_drop, type_drop, id_drop, model_drop], outputs=load_outputs)
320
- model_drop.change(fn=load_chart_data, inputs=[source_drop, type_drop, id_drop, model_drop], outputs=load_outputs)
321
-
322
- qa_outputs = [current_qa_id, orig_q, orig_a, status_radio, issue_type, mod_q, mod_a, comment]
323
- qa_selector.change(fn=on_qa_select, inputs=[qa_selector], outputs=qa_outputs)
 
 
 
 
 
324
 
325
- nav_outputs = [source_drop, type_drop, id_drop, model_drop]
326
- prev_btn.click(fn=navigate_prev, outputs=nav_outputs)
327
- next_btn.click(fn=navigate_next, outputs=nav_outputs)
 
 
 
 
 
 
328
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  save_btn.click(
330
- fn=save_review_handler,
331
- inputs=[current_qa_id, id_drop, source_drop, type_drop, model_drop, orig_q, orig_a, status_radio, mod_q, mod_a, issue_type, comment, reviewer],
332
- outputs=[save_msg]
333
- ).then(fn=lambda: gr.Textbox(visible=True), outputs=[save_msg])
334
-
335
- export_btn.click(fn=export_reviews_handler, outputs=[download_file]).then(fn=lambda: gr.File(visible=True), outputs=[download_file])
336
-
337
- return app
338
 
339
  if __name__ == "__main__":
340
  app = create_ui()
341
- app.launch(server_name="0.0.0.0", server_port=7860, show_api=False)
 
 
 
 
 
 
 
 
 
 
1
  import gradio as gr
 
 
2
  import json
 
3
  import base64
4
+ from data_manager import data_manager
5
 
6
+ # ============== 状态管理 (封装逻辑) ==============
7
+ 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):
18
+ self.current_idx = new_idx
19
+ return self.all_paths[new_idx]
20
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
+ nav_state = ReviewState()
 
 
23
 
24
+ # ============== 工具函数 ==============
25
+ def to_html_frame(html_content):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  if not html_content:
27
+ return '<div style="padding:20px;text-align:center;">请选择数据进行加载</div>'
28
+ b64_content = base64.b64encode(html_content.encode('utf-8')).decode('utf-8')
29
+ return f'<iframe src="data:text/html;base64,{b64_content}" style="width:100%;height:600px;border:none;"></iframe>'
30
+
31
+ # ============== 交互逻辑 ==============
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):
39
+ charts = data_manager.get_chart_list(source, c_type)
40
+ struct = data_manager.get_dataset_structure()
41
+ models = struct.get('sources', {}).get(source, {}).get('chart_types', {}).get(c_type, {}).get('models', [])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  return (
43
+ gr.update(choices=charts, value=charts[0] if charts else None),
44
+ gr.update(choices=models, value=models[0] if models else None)
 
 
 
 
45
  )
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,
75
+ qa_json,
76
+ stats_str,
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({}) # 占位 review_store
81
+ ]
82
+
83
+ def handle_qa_select(selection, qa_json):
84
+ if not selection or not qa_json:
85
+ return [""] * 8
86
  try:
87
+ qas = json.loads(qa_json)
88
+ idx = int(selection.split(":")[0][1:]) - 1
89
+ curr = qas[idx]
90
+ return [curr['id'], curr['q'], curr['a'], "correct", "无", "", "", ""]
91
+ except:
92
+ return [""] * 8
93
+
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
 
101
+ gr.Markdown("## 📑 图表问答数据集审核终端")
 
 
 
 
 
 
 
 
 
 
 
102
 
103
  with gr.Row():
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  with gr.Column(scale=4):
105
+ with gr.Row():
106
+ src_dd = gr.Dropdown(label="数据源", choices=["None"])
107
+ typ_dd = gr.Dropdown(label="图表类型")
108
+ id_dd = gr.Dropdown(label="图表 ID")
109
+ mdl_dd = gr.Dropdown(label="评估模型")
110
 
111
+ chart_view = gr.HTML(value='<div style="height:500px; background:#f0f0f0;"></div>')
112
+ path_info = gr.Text(label="当前路径", interactive=False)
113
 
114
+ with gr.Column(scale=2):
115
+ with gr.Group():
116
+ stats_txt = gr.Text(label="统计信息", interactive=False)
117
+ prog_txt = gr.Text(label="审核进度", interactive=False)
118
 
119
+ with gr.Accordion("元数据解析", open=False):
120
+ meta_md = gr.Markdown()
121
+
122
  gr.Markdown("---")
123
+ qa_radio = gr.Radio(label="题目列表", choices=[])
 
 
 
 
 
 
 
 
 
 
 
 
 
124
 
125
+ with gr.Group():
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
+ choices=[("正确", "correct"), ("错误", "incorrect"), ("优化", "modified")],
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],
165
+ outputs=[curr_qid, q_disp, a_disp, status_opt, err_type, gr.State(), gr.State(), comment]
166
+ )
167
+
168
+ # 导航逻辑
169
+ def navigate(direction):
170
+ target = nav_state.get_nav_target(direction)
171
+ if target:
172
+ return [
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
+ prev_btn.click(lambda: navigate(-1), outputs=[src_dd, typ_dd, id_dd, mdl_dd])
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: return "无效操作"
186
+ data_manager.save_review({
187
+ "qa_id": qid, "chart_id": cid, "source": src,
188
+ "status": status, "comment": cmt
189
+ })
190
+ return "已保存"
191
+
192
  save_btn.click(
193
+ quick_save,
194
+ inputs=[curr_qid, id_dd, src_dd, status_opt, comment],
195
+ outputs=[gr.Text(visible=False)]
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
+ )