Laramie2 commited on
Commit
9d4ee7c
·
verified ·
1 Parent(s): cc2408d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +117 -64
app.py CHANGED
@@ -123,13 +123,11 @@ def get_tree_html(dir_path):
123
  for item in items:
124
  item_path = os.path.join(current_path, item)
125
  if os.path.isdir(item_path):
126
- # 文件夹节点 (可折叠)
127
  html += f'<details style="margin-left: 15px; cursor: pointer; margin-top: 4px;"><summary style="outline: none; color: #C084FC;">📁 <b>{item}</b></summary>'
128
  inner_html = build_html(item_path)
129
  html += inner_html if inner_html else "<div style='margin-left: 20px; color: #888; font-size: 12px;'><i>Empty</i></div>"
130
  html += '</details>'
131
  else:
132
- # 文件节点
133
  html += f'<div style="margin-left: 18px; padding-left: 12px; border-left: 1px dotted #A855F7; margin-top: 4px; color: #DAB2FF;">📄 {item}</div>'
134
  return html
135
 
@@ -155,10 +153,32 @@ def get_debug_info():
155
  """
156
  return html
157
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  def save_pdf(file):
159
- if file is None: return "❌ Please select a PDF first", get_debug_info()
160
  try:
161
- # 先清空原有 PDF,确保只有最新的一份
162
  for f in os.listdir(PAPERS_DIR):
163
  file_to_del = os.path.join(PAPERS_DIR, f)
164
  if os.path.isfile(file_to_del):
@@ -166,72 +186,54 @@ def save_pdf(file):
166
 
167
  file_path = os.path.join(PAPERS_DIR, os.path.basename(file.name))
168
  shutil.copy(file.name, file_path)
169
- return f"✅ Saved: {os.path.basename(file.name)}", get_debug_info()
 
170
  except Exception as e:
171
- return f"❌ Error: {str(e)}", get_debug_info()
172
 
173
  def clear_pdf():
174
- """Delete PDF and its corresponding mineru output directory"""
175
  try:
176
  deleted_files = []
177
  deleted_dirs = []
178
 
179
- # 1. 记录要被删除的 PDF 文件名(用于匹配输出目录)
180
  pdf_names = [f for f in os.listdir(PAPERS_DIR) if f.endswith('.pdf')]
181
 
182
- # 2. 删除 papers 目录下的 PDF
183
  for f in os.listdir(PAPERS_DIR):
184
  file_to_del = os.path.join(PAPERS_DIR, f)
185
  if os.path.isfile(file_to_del):
186
  os.remove(file_to_del)
187
  deleted_files.append(f)
188
 
189
- # 3. 删除 mineru_outputs 下的同名文件夹
190
  if os.path.exists(OUTPUT_DIR):
191
  for pdf_name in pdf_names:
192
  base_name = os.path.splitext(pdf_name)[0]
193
- base_name_us = base_name.replace(" ", "_") # 兼容 Mineru 可能会把空格转为下划线
194
 
195
  for item in os.listdir(OUTPUT_DIR):
196
- # 匹配严格同名,或者下划线名称,或者带 .pdf 后缀的文件夹
197
  if item in [base_name, base_name_us, pdf_name]:
198
  dir_to_del = os.path.join(OUTPUT_DIR, item)
199
  if os.path.isdir(dir_to_del):
200
  shutil.rmtree(dir_to_del)
201
  deleted_dirs.append(item)
202
 
203
- # 4. 删除可能遗留的打包压缩文件
204
  if os.path.exists(ZIP_OUTPUT_PATH):
205
  os.remove(ZIP_OUTPUT_PATH)
206
 
 
 
207
  if deleted_files or deleted_dirs:
208
- return f"🗑️ Workspace cleared (Deleted: {len(deleted_files)} PDF, {len(deleted_dirs)} Output Folder)", get_debug_info()
209
- return "ℹ️ Workspace is already empty", get_debug_info()
210
  except Exception as e:
211
- return f"❌ Error deleting file: {str(e)}", get_debug_info()
212
-
213
- def save_api_settings(api_key, api_base_url=None):
214
- if not api_key: return "❌ Key cannot be empty", get_debug_info()
215
- try:
216
- config = {}
217
- if os.path.exists(CONFIG_PATH):
218
- with open(CONFIG_PATH, "r", encoding="utf-8") as f:
219
- config = yaml.safe_load(f) or {}
220
- config.setdefault("api_keys", {})["gemini_api_key"] = api_key
221
- if api_base_url:
222
- config["api_base_url"] = api_base_url
223
- with open(CONFIG_PATH, "w", encoding="utf-8") as f:
224
- yaml.dump(config, f, allow_unicode=True)
225
- success_msg = "✅ Key saved"
226
- if api_base_url:
227
- success_msg += ", Base URL updated"
228
- return success_msg, get_debug_info()
229
- except Exception as e:
230
- return f"❌ Error: {str(e)}", get_debug_info()
231
 
232
  def run_mineru_parsing_and_dag_gen():
 
 
 
233
  if not os.path.exists(PAPERS_DIR) or not any(f.endswith('.pdf') for f in os.listdir(PAPERS_DIR)):
234
- yield "❌ No PDF file found", get_debug_info(), "No execution logs."
235
  return
236
 
237
  full_log = ""
@@ -244,40 +246,47 @@ def run_mineru_parsing_and_dag_gen():
244
 
245
  command_mineru = ["mineru", "-p", PAPERS_DIR, "-o", OUTPUT_DIR]
246
  full_log += "--- Mineru Executing ---\n"
247
- yield "⏳ Executing Mineru parsing...", get_debug_info(), full_log
 
248
 
249
  process_mineru = subprocess.Popen(
250
  command_mineru, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
251
  )
252
  for line in iter(process_mineru.stdout.readline, ''):
253
  full_log += line
254
- yield "⏳ Executing Mineru parsing...", get_debug_info(), full_log
255
  process_mineru.stdout.close()
256
  returncode_mineru = process_mineru.wait()
257
 
258
  if returncode_mineru != 0:
259
- yield f"❌ Mineru parsing failed (Exit Code: {returncode_mineru})", get_debug_info(), full_log
260
  return
261
 
262
  command_dag = [sys.executable, "gen_dag.py"]
263
  full_log += "\n--- DAG Gen Executing ---\n"
264
- yield "⏳ Mineru parsing complete, executing DAG generation...", get_debug_info(), full_log
265
 
266
  process_dag = subprocess.Popen(
267
  command_dag, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
268
  )
269
  for line in iter(process_dag.stdout.readline, ''):
270
  full_log += line
271
- yield "⏳ Executing DAG generation...", get_debug_info(), full_log
272
  process_dag.stdout.close()
273
  returncode_dag = process_dag.wait()
274
 
275
- status = "✅ PDF parsing & DAG generation fully completed" if returncode_dag == 0 else f"❌ DAG generation failed (Exit Code: {returncode_dag})"
276
- yield status, get_debug_info(), full_log
 
 
 
 
 
 
277
 
278
  except Exception as e:
279
  error_log = full_log + f"\n[Global Exception] Exception occurred:\n{str(e)}"
280
- yield "❌ Execution Exception", get_debug_info(), error_log
281
 
282
 
283
  def run_final_generation(task_type="all"):
@@ -303,14 +312,12 @@ def run_final_generation(task_type="all"):
303
  q = queue.Queue()
304
  processes = []
305
 
306
- # 将标准输出推送到队列的工作线程
307
  def enqueue_output(out, script_name):
308
  for line in iter(out.readline, ''):
309
  q.put((script_name, line))
310
  out.close()
311
 
312
  try:
313
- # 并发启动所有脚本
314
  for script in scripts_to_run:
315
  p = subprocess.Popen(
316
  [sys.executable, script],
@@ -324,18 +331,15 @@ def run_final_generation(task_type="all"):
324
  t.daemon = True
325
  t.start()
326
 
327
- # 从队列中实时读取日志并更新 UI
328
  active_processes = len(processes)
329
  while active_processes > 0 or not q.empty():
330
  try:
331
- # 设定 timeout,这样即便没有日志产生也能循环检查进程是否结束
332
  script_name, line = q.get(timeout=0.1)
333
  full_log += f"[{script_name}] {line}"
334
  yield f"⏳ Generating {task_type.upper()}...", get_debug_info(), full_log, gr.update(visible=False)
335
  except queue.Empty:
336
  active_processes = sum(1 for _, p in processes if p.poll() is None)
337
 
338
- # 检查最终执行结果
339
  success = True
340
  for script, p in processes:
341
  if p.returncode != 0:
@@ -346,7 +350,6 @@ def run_final_generation(task_type="all"):
346
  yield f"❌ {task_type.upper()} contains failed tasks, please check logs", get_debug_info(), full_log, gr.update(visible=False)
347
  return
348
 
349
- # 全部成功后进行压缩
350
  full_log += "\n📦 Zipping output directory...\n"
351
  yield f"⏳ Zipping outputs...", get_debug_info(), full_log, gr.update(visible=False)
352
 
@@ -510,6 +513,21 @@ body, .gradio-container {
510
  }
511
  .action-btn:active { transform: translateY(2px) scale(0.98) !important; box-shadow: 0 2px 10px rgba(147, 51, 234, 0.2) !important; }
512
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  .log-box textarea { font-family: 'IBM Plex Mono', monospace !important; font-size: 13px !important; background-color: #1e1e1e !important; color: #DAB2FF !important; border: 1px solid #C084FC !important; border-radius: 8px !important; }
514
  .status-text textarea { background-color: transparent !important; border: none !important; box-shadow: none !important; font-weight: 600 !important; color: #6B21A8 !important; }
515
  .dark .status-text textarea { color: #C084FC !important; }
@@ -518,12 +536,15 @@ body, .gradio-container {
518
  ::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #A855F7, #C084FC); border-radius: 4px; }
519
  ::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, #9333EA, #A855F7); }
520
 
521
- /* 使折叠树中的 details 和 summary 更加平滑 */
522
  details > summary { transition: color 0.2s ease; }
523
  details > summary:hover { color: #E9D5FF !important; }
524
  """
525
 
526
  with gr.Blocks(theme=purple_theme, css=custom_css) as demo:
 
 
 
 
527
  with gr.Column(elem_id="col-container"):
528
  gr.Markdown("# **PaperX Platform**", elem_id="main-title")
529
  gr.Markdown("One-click parsing of academic PDFs, DAG structuring, and multi-modal asset generation.", elem_id="subtitle")
@@ -539,6 +560,10 @@ with gr.Blocks(theme=purple_theme, css=custom_css) as demo:
539
  key_input = gr.Textbox(label="API Key", type="password", placeholder="sk-...", scale=1)
540
  api_base_url_input = gr.Textbox(label="Base URL (Optional)", placeholder="https://api.example.com", scale=1)
541
  key_btn = gr.Button("💾 Save API Configuration")
 
 
 
 
542
 
543
  # 2. Document Parsing
544
  with gr.Group(elem_classes="gradio-group"):
@@ -550,7 +575,8 @@ with gr.Blocks(theme=purple_theme, css=custom_css) as demo:
550
  elem_id="pdf-upload-box"
551
  )
552
 
553
- parse_btn = gr.Button("🚀 Start Mineru & DAG Extraction", elem_classes="primary-action-btn")
 
554
 
555
  parse_status = gr.Textbox(
556
  show_label=False, placeholder="Waiting for document upload...", lines=1, interactive=False, elem_classes="status-text"
@@ -562,11 +588,13 @@ with gr.Blocks(theme=purple_theme, css=custom_css) as demo:
562
  gr.Markdown("Generate final formats based on DAG structure:")
563
 
564
  with gr.Row(elem_classes="action-row"):
565
- gen_ppt_btn = gr.Button("📊 Gen PPT", elem_classes="action-btn")
566
- gen_poster_btn = gr.Button("🖼️ Gen Poster", elem_classes="action-btn")
567
- gen_pr_btn = gr.Button("📰 Gen PR", elem_classes="action-btn")
 
568
 
569
- gen_all_btn = gr.Button("✨ Generate All Assets (ALL)", elem_classes="primary-action-btn")
 
570
 
571
  # ================= RIGHT COLUMN: OUTPUTS & LOGS =================
572
  with gr.Column(scale=1):
@@ -590,21 +618,46 @@ with gr.Blocks(theme=purple_theme, css=custom_css) as demo:
590
 
591
  with gr.Tab("🔍 System Snapshot"):
592
  refresh_btn = gr.Button("🔄 Refresh Directory Tree")
593
- # 这里改为 HTML 组件,以便渲染我们的 details/summary 折叠树
594
  debug_view = gr.HTML(
595
  value=get_debug_info()
596
  )
597
 
598
  # ================= LOGIC BINDINGS =================
599
- key_btn.click(fn=save_api_settings, inputs=[key_input, api_base_url_input], outputs=[parse_status, debug_view])
600
 
601
- pdf_input.upload(fn=save_pdf, inputs=pdf_input, outputs=[parse_status, debug_view])
602
- # 彻底清除:不仅清除 PDF,还清除 mineru output 中的对应文件夹
603
- pdf_input.clear(fn=clear_pdf, outputs=[parse_status, debug_view])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
604
 
605
- parse_btn.click(fn=run_mineru_parsing_and_dag_gen, outputs=[parse_status, debug_view, cmd_logs])
 
 
 
 
 
 
 
 
 
 
 
 
606
 
607
- # 为了确保 Gradio 生成器模式正确工作,拆分为显式的封装函数
608
  def trigger_gen_ppt(): yield from run_final_generation("ppt")
609
  def trigger_gen_poster(): yield from run_final_generation("poster")
610
  def trigger_gen_pr(): yield from run_final_generation("pr")
@@ -618,4 +671,4 @@ with gr.Blocks(theme=purple_theme, css=custom_css) as demo:
618
  refresh_btn.click(fn=get_debug_info, outputs=debug_view)
619
 
620
  if __name__ == "__main__":
621
- demo.launch()
 
123
  for item in items:
124
  item_path = os.path.join(current_path, item)
125
  if os.path.isdir(item_path):
 
126
  html += f'<details style="margin-left: 15px; cursor: pointer; margin-top: 4px;"><summary style="outline: none; color: #C084FC;">📁 <b>{item}</b></summary>'
127
  inner_html = build_html(item_path)
128
  html += inner_html if inner_html else "<div style='margin-left: 20px; color: #888; font-size: 12px;'><i>Empty</i></div>"
129
  html += '</details>'
130
  else:
 
131
  html += f'<div style="margin-left: 18px; padding-left: 12px; border-left: 1px dotted #A855F7; margin-top: 4px; color: #DAB2FF;">📄 {item}</div>'
132
  return html
133
 
 
153
  """
154
  return html
155
 
156
+ def save_api_settings(api_key, api_base_url=None):
157
+ if not api_key:
158
+ return "❌ Key cannot be empty", get_debug_info(), False
159
+ try:
160
+ config = {}
161
+ if os.path.exists(CONFIG_PATH):
162
+ with open(CONFIG_PATH, "r", encoding="utf-8") as f:
163
+ config = yaml.safe_load(f) or {}
164
+ config.setdefault("api_keys", {})["gemini_api_key"] = api_key
165
+ if api_base_url:
166
+ config["api_base_url"] = api_base_url
167
+ with open(CONFIG_PATH, "w", encoding="utf-8") as f:
168
+ yaml.dump(config, f, allow_unicode=True)
169
+
170
+ success_msg = "✅ Key saved"
171
+ if api_base_url:
172
+ success_msg += ", Base URL updated"
173
+
174
+ # 返回成功消息和 True (表示 API 已就绪)
175
+ return success_msg, get_debug_info(), True
176
+ except Exception as e:
177
+ return f"❌ Error: {str(e)}", get_debug_info(), False
178
+
179
  def save_pdf(file):
180
+ if file is None: return "❌ Please select a PDF first", get_debug_info(), False
181
  try:
 
182
  for f in os.listdir(PAPERS_DIR):
183
  file_to_del = os.path.join(PAPERS_DIR, f)
184
  if os.path.isfile(file_to_del):
 
186
 
187
  file_path = os.path.join(PAPERS_DIR, os.path.basename(file.name))
188
  shutil.copy(file.name, file_path)
189
+ # 返回成功消息和 True (表示 PDF 已就绪)
190
+ return f"✅ Saved: {os.path.basename(file.name)}", get_debug_info(), True
191
  except Exception as e:
192
+ return f"❌ Error: {str(e)}", get_debug_info(), False
193
 
194
  def clear_pdf():
195
+ """清空 PDF 并锁定后续所有步骤"""
196
  try:
197
  deleted_files = []
198
  deleted_dirs = []
199
 
 
200
  pdf_names = [f for f in os.listdir(PAPERS_DIR) if f.endswith('.pdf')]
201
 
 
202
  for f in os.listdir(PAPERS_DIR):
203
  file_to_del = os.path.join(PAPERS_DIR, f)
204
  if os.path.isfile(file_to_del):
205
  os.remove(file_to_del)
206
  deleted_files.append(f)
207
 
 
208
  if os.path.exists(OUTPUT_DIR):
209
  for pdf_name in pdf_names:
210
  base_name = os.path.splitext(pdf_name)[0]
211
+ base_name_us = base_name.replace(" ", "_")
212
 
213
  for item in os.listdir(OUTPUT_DIR):
 
214
  if item in [base_name, base_name_us, pdf_name]:
215
  dir_to_del = os.path.join(OUTPUT_DIR, item)
216
  if os.path.isdir(dir_to_del):
217
  shutil.rmtree(dir_to_del)
218
  deleted_dirs.append(item)
219
 
 
220
  if os.path.exists(ZIP_OUTPUT_PATH):
221
  os.remove(ZIP_OUTPUT_PATH)
222
 
223
+ disable_btn = gr.update(interactive=False)
224
+ # 返回:状态文本, debug视窗, pdf_ready=False, 以及锁定4个生成按钮
225
  if deleted_files or deleted_dirs:
226
+ return f"🗑️ Workspace cleared", get_debug_info(), False, disable_btn, disable_btn, disable_btn, disable_btn
227
+ return "ℹ️ Workspace is already empty", get_debug_info(), False, disable_btn, disable_btn, disable_btn, disable_btn
228
  except Exception as e:
229
+ return f"❌ Error deleting file: {str(e)}", get_debug_info(), False, gr.update(), gr.update(), gr.update(), gr.update()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
  def run_mineru_parsing_and_dag_gen():
232
+ no_change = gr.update()
233
+ disable_btn = gr.update(interactive=False)
234
+
235
  if not os.path.exists(PAPERS_DIR) or not any(f.endswith('.pdf') for f in os.listdir(PAPERS_DIR)):
236
+ yield "❌ No PDF file found", get_debug_info(), "No execution logs.", no_change, no_change, no_change, no_change
237
  return
238
 
239
  full_log = ""
 
246
 
247
  command_mineru = ["mineru", "-p", PAPERS_DIR, "-o", OUTPUT_DIR]
248
  full_log += "--- Mineru Executing ---\n"
249
+ # 运行中保持按钮状态不变
250
+ yield "⏳ Executing Mineru parsing...", get_debug_info(), full_log, no_change, no_change, no_change, no_change
251
 
252
  process_mineru = subprocess.Popen(
253
  command_mineru, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
254
  )
255
  for line in iter(process_mineru.stdout.readline, ''):
256
  full_log += line
257
+ yield "⏳ Executing Mineru parsing...", get_debug_info(), full_log, no_change, no_change, no_change, no_change
258
  process_mineru.stdout.close()
259
  returncode_mineru = process_mineru.wait()
260
 
261
  if returncode_mineru != 0:
262
+ yield f"❌ Mineru parsing failed (Exit Code: {returncode_mineru})", get_debug_info(), full_log, disable_btn, disable_btn, disable_btn, disable_btn
263
  return
264
 
265
  command_dag = [sys.executable, "gen_dag.py"]
266
  full_log += "\n--- DAG Gen Executing ---\n"
267
+ yield "⏳ Mineru parsing complete, executing DAG generation...", get_debug_info(), full_log, no_change, no_change, no_change, no_change
268
 
269
  process_dag = subprocess.Popen(
270
  command_dag, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
271
  )
272
  for line in iter(process_dag.stdout.readline, ''):
273
  full_log += line
274
+ yield "⏳ Executing DAG generation...", get_debug_info(), full_log, no_change, no_change, no_change, no_change
275
  process_dag.stdout.close()
276
  returncode_dag = process_dag.wait()
277
 
278
+ # 解析完全成功,解锁第三部分的按钮
279
+ if returncode_dag == 0:
280
+ status = "✅ PDF parsing & DAG generation fully completed"
281
+ enable_btn = gr.update(interactive=True)
282
+ yield status, get_debug_info(), full_log, enable_btn, enable_btn, enable_btn, enable_btn
283
+ else:
284
+ status = f"❌ DAG generation failed (Exit Code: {returncode_dag})"
285
+ yield status, get_debug_info(), full_log, disable_btn, disable_btn, disable_btn, disable_btn
286
 
287
  except Exception as e:
288
  error_log = full_log + f"\n[Global Exception] Exception occurred:\n{str(e)}"
289
+ yield "❌ Execution Exception", get_debug_info(), error_log, disable_btn, disable_btn, disable_btn, disable_btn
290
 
291
 
292
  def run_final_generation(task_type="all"):
 
312
  q = queue.Queue()
313
  processes = []
314
 
 
315
  def enqueue_output(out, script_name):
316
  for line in iter(out.readline, ''):
317
  q.put((script_name, line))
318
  out.close()
319
 
320
  try:
 
321
  for script in scripts_to_run:
322
  p = subprocess.Popen(
323
  [sys.executable, script],
 
331
  t.daemon = True
332
  t.start()
333
 
 
334
  active_processes = len(processes)
335
  while active_processes > 0 or not q.empty():
336
  try:
 
337
  script_name, line = q.get(timeout=0.1)
338
  full_log += f"[{script_name}] {line}"
339
  yield f"⏳ Generating {task_type.upper()}...", get_debug_info(), full_log, gr.update(visible=False)
340
  except queue.Empty:
341
  active_processes = sum(1 for _, p in processes if p.poll() is None)
342
 
 
343
  success = True
344
  for script, p in processes:
345
  if p.returncode != 0:
 
350
  yield f"❌ {task_type.upper()} contains failed tasks, please check logs", get_debug_info(), full_log, gr.update(visible=False)
351
  return
352
 
 
353
  full_log += "\n📦 Zipping output directory...\n"
354
  yield f"⏳ Zipping outputs...", get_debug_info(), full_log, gr.update(visible=False)
355
 
 
513
  }
514
  .action-btn:active { transform: translateY(2px) scale(0.98) !important; box-shadow: 0 2px 10px rgba(147, 51, 234, 0.2) !important; }
515
 
516
+ /* ======== CSS FOR DISABLED BUTTONS ======== */
517
+ .primary-action-btn:disabled, .action-btn:disabled {
518
+ background: #e5e7eb !important;
519
+ color: #9ca3af !important;
520
+ box-shadow: none !important;
521
+ transform: none !important;
522
+ cursor: not-allowed !important;
523
+ border: 1px solid #d1d5db !important;
524
+ }
525
+ .dark .primary-action-btn:disabled, .dark .action-btn:disabled {
526
+ background: #374151 !important;
527
+ color: #6b7280 !important;
528
+ border: 1px solid #4b5563 !important;
529
+ }
530
+
531
  .log-box textarea { font-family: 'IBM Plex Mono', monospace !important; font-size: 13px !important; background-color: #1e1e1e !important; color: #DAB2FF !important; border: 1px solid #C084FC !important; border-radius: 8px !important; }
532
  .status-text textarea { background-color: transparent !important; border: none !important; box-shadow: none !important; font-weight: 600 !important; color: #6B21A8 !important; }
533
  .dark .status-text textarea { color: #C084FC !important; }
 
536
  ::-webkit-scrollbar-thumb { background: linear-gradient(135deg, #A855F7, #C084FC); border-radius: 4px; }
537
  ::-webkit-scrollbar-thumb:hover { background: linear-gradient(135deg, #9333EA, #A855F7); }
538
 
 
539
  details > summary { transition: color 0.2s ease; }
540
  details > summary:hover { color: #E9D5FF !important; }
541
  """
542
 
543
  with gr.Blocks(theme=purple_theme, css=custom_css) as demo:
544
+ # 定义全局状态变量,用于实现依赖解锁
545
+ api_saved_state = gr.State(False)
546
+ pdf_ready_state = gr.State(False)
547
+
548
  with gr.Column(elem_id="col-container"):
549
  gr.Markdown("# **PaperX Platform**", elem_id="main-title")
550
  gr.Markdown("One-click parsing of academic PDFs, DAG structuring, and multi-modal asset generation.", elem_id="subtitle")
 
560
  key_input = gr.Textbox(label="API Key", type="password", placeholder="sk-...", scale=1)
561
  api_base_url_input = gr.Textbox(label="Base URL (Optional)", placeholder="https://api.example.com", scale=1)
562
  key_btn = gr.Button("💾 Save API Configuration")
563
+
564
+ api_status = gr.Textbox(
565
+ show_label=False, placeholder="Waiting for API configuration...", lines=1, interactive=False, elem_classes="status-text"
566
+ )
567
 
568
  # 2. Document Parsing
569
  with gr.Group(elem_classes="gradio-group"):
 
575
  elem_id="pdf-upload-box"
576
  )
577
 
578
+ # 默认锁定
579
+ parse_btn = gr.Button("🚀 Start Mineru & DAG Extraction", elem_classes="primary-action-btn", interactive=False)
580
 
581
  parse_status = gr.Textbox(
582
  show_label=False, placeholder="Waiting for document upload...", lines=1, interactive=False, elem_classes="status-text"
 
588
  gr.Markdown("Generate final formats based on DAG structure:")
589
 
590
  with gr.Row(elem_classes="action-row"):
591
+ # 默认锁定
592
+ gen_ppt_btn = gr.Button("📊 Gen PPT", elem_classes="action-btn", interactive=False)
593
+ gen_poster_btn = gr.Button("🖼️ Gen Poster", elem_classes="action-btn", interactive=False)
594
+ gen_pr_btn = gr.Button("📰 Gen PR", elem_classes="action-btn", interactive=False)
595
 
596
+ # 默认锁定
597
+ gen_all_btn = gr.Button("✨ Generate All Assets (ALL)", elem_classes="primary-action-btn", interactive=False)
598
 
599
  # ================= RIGHT COLUMN: OUTPUTS & LOGS =================
600
  with gr.Column(scale=1):
 
618
 
619
  with gr.Tab("🔍 System Snapshot"):
620
  refresh_btn = gr.Button("🔄 Refresh Directory Tree")
 
621
  debug_view = gr.HTML(
622
  value=get_debug_info()
623
  )
624
 
625
  # ================= LOGIC BINDINGS =================
 
626
 
627
+ # [步骤 1] 配置 API
628
+ # 成功后不仅更新 UI 文本,还会将 api_saved_state 设置为 True
629
+ key_btn.click(
630
+ fn=save_api_settings,
631
+ inputs=[key_input, api_base_url_input],
632
+ outputs=[api_status, debug_view, api_saved_state]
633
+ )
634
+
635
+ # [步骤 2.1] 上传 PDF
636
+ # 成功后更新文本,并将 pdf_ready_state 设置为 True
637
+ pdf_input.upload(fn=save_pdf, inputs=pdf_input, outputs=[parse_status, debug_view, pdf_ready_state])
638
+
639
+ # [步骤 2.2] 清除 PDF
640
+ # 会锁定第四步的所有生成按钮,同时将 pdf_ready_state 重置为 False
641
+ pdf_input.clear(
642
+ fn=clear_pdf,
643
+ outputs=[parse_status, debug_view, pdf_ready_state, gen_ppt_btn, gen_poster_btn, gen_pr_btn, gen_all_btn]
644
+ )
645
 
646
+ # [依赖检查] api 状态或 pdf 状态发生变化时,检查是否解锁"开始解析"按钮
647
+ def check_parse_btn_ready(api_ready, pdf_ready):
648
+ return gr.update(interactive=(api_ready and pdf_ready))
649
+
650
+ api_saved_state.change(fn=check_parse_btn_ready, inputs=[api_saved_state, pdf_ready_state], outputs=parse_btn)
651
+ pdf_ready_state.change(fn=check_parse_btn_ready, inputs=[api_saved_state, pdf_ready_state], outputs=parse_btn)
652
+
653
+ # [步骤 2.3] 执行解析
654
+ # 如果完全成功,函数最后的一个 yield 会连带输出 interactive=True 给这4个生成按钮
655
+ parse_btn.click(
656
+ fn=run_mineru_parsing_and_dag_gen,
657
+ outputs=[parse_status, debug_view, cmd_logs, gen_ppt_btn, gen_poster_btn, gen_pr_btn, gen_all_btn]
658
+ )
659
 
660
+ # [步骤 3] 资产生成
661
  def trigger_gen_ppt(): yield from run_final_generation("ppt")
662
  def trigger_gen_poster(): yield from run_final_generation("poster")
663
  def trigger_gen_pr(): yield from run_final_generation("pr")
 
671
  refresh_btn.click(fn=get_debug_info, outputs=debug_view)
672
 
673
  if __name__ == "__main__":
674
+ demo.launch(debug=True)