|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| import sys
|
|
|
|
|
|
|
|
|
| try:
|
| sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
| sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
| except AttributeError:
|
| pass
|
|
|
| import gradio as gr
|
| import json
|
| import os
|
| import uuid
|
| import glob
|
| import pandas as pd
|
| import traceback
|
| import datetime
|
| import time
|
| from agent import agent
|
|
|
|
|
| if not os.environ.get("MISTRAL_API_KEY"):
|
| print("\n" + "!"*80)
|
| print("CRITICAL WARNING: MISTRAL_API_KEY environment variable is NOT set.")
|
| print("The agent will fail with a 401 Unauthorized error when calling Mistral.")
|
| print("!"*80 + "\n")
|
|
|
| print(f"[app.py] Starting with Gradio {gr.__version__}")
|
|
|
|
|
|
|
|
|
| REVIEW_COLUMNS = [
|
| "#", "Topic Label", "Top Evidence Sentence",
|
| "Sent.", "Papers", "Approve", "Rename To",
|
| ]
|
|
|
| EMPTY_REVIEW_DF = pd.DataFrame(
|
| columns=REVIEW_COLUMNS,
|
| data=[["", "", "", 0, 0, False, ""]],
|
| )
|
|
|
| DOWNLOAD_FILES = [
|
| "narrative.txt", "comparison.csv", "themes.json",
|
| "taxonomy_map.json", "labels_abstract.json", "labels_title.json",
|
| ]
|
|
|
|
|
| CHECKPOINT_FILES = [
|
| "loaded_data.csv",
|
| "summaries_abstract.json", "summaries_title.json",
|
| "emb_abstract.npy", "emb_title.npy",
|
| "labels_abstract.json", "labels_title.json",
|
| "themes.json", "themes_abstract.json", "themes_title.json",
|
| "taxonomy_map.json", "comparison.csv", "narrative.txt",
|
| "chart_abstract_intertopic.html", "chart_abstract_bars.html",
|
| "chart_abstract_hierarchy.html", "chart_abstract_heatmap.html",
|
| "chart_title_intertopic.html", "chart_title_bars.html",
|
| "chart_title_hierarchy.html", "chart_title_heatmap.html",
|
| ]
|
|
|
| CHART_OPTIONS = [
|
| ("Intertopic Map β Abstract", "chart_abstract_intertopic.html"),
|
| ("Frequency Bars β Abstract", "chart_abstract_bars.html"),
|
| ("Hierarchy / Treemap β Abstract", "chart_abstract_hierarchy.html"),
|
| ("Similarity Heatmap β Abstract", "chart_abstract_heatmap.html"),
|
| ("Intertopic Map β Title", "chart_title_intertopic.html"),
|
| ("Frequency Bars β Title", "chart_title_bars.html"),
|
| ("Hierarchy / Treemap β Title", "chart_title_hierarchy.html"),
|
| ("Similarity Heatmap β Title", "chart_title_heatmap.html"),
|
| ]
|
|
|
| PHASE_LABELS = [
|
| ("1","β Load"), ("2","β‘ Codes"), ("3","β’ Themes"),
|
| ("4","β£ Review"), ("5","β€ Names"), ("5.5","β€Β½ PAJAIS"), ("6","β₯ Report"),
|
| ]
|
|
|
|
|
|
|
| CORRUPT_HISTORY_SIGNALS = [
|
| "INVALID_CHAT_HISTORY",
|
| "ToolMessage",
|
| "tool_calls that do not have a corresponding",
|
| ]
|
|
|
| CSS = """
|
| @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600;700&family=IBM+Plex+Sans:wght@300;400;500;600&display=swap');
|
|
|
| :root {
|
| --bg-base: #0b0d14;
|
| --bg-surface: #111420;
|
| --bg-card: #161924;
|
| --border: #1e2235;
|
| --accent: #4f8ef7;
|
| --accent-dim: #4f8ef722;
|
| --green: #00e5a0;
|
| --text: #d4d8f0;
|
| --text-muted: #666a88;
|
| --radius: 10px;
|
| }
|
|
|
| body, .gradio-container {
|
| background: var(--bg-base) !important;
|
| color: var(--text) !important;
|
| font-family: 'IBM Plex Sans', sans-serif !important;
|
| }
|
|
|
| /* Section headers */
|
| .section-header {
|
| font-family: 'IBM Plex Mono', monospace;
|
| font-size: 11px;
|
| font-weight: 700;
|
| letter-spacing: 0.2em;
|
| text-transform: uppercase;
|
| color: var(--accent);
|
| padding: 6px 0 14px;
|
| border-bottom: 1px solid var(--border);
|
| margin-bottom: 16px;
|
| }
|
|
|
| /* Override Gradio panels */
|
| .gr-panel, .gr-box, .gradio-group {
|
| background: var(--bg-card) !important;
|
| border: 1px solid var(--border) !important;
|
| border-radius: var(--radius) !important;
|
| }
|
|
|
| /* Buttons */
|
| button.primary {
|
| background: var(--accent) !important;
|
| color: #fff !important;
|
| font-family: 'IBM Plex Mono', monospace !important;
|
| font-size: 12px !important;
|
| letter-spacing: 0.08em !important;
|
| border-radius: 6px !important;
|
| border: none !important;
|
| padding: 10px 20px !important;
|
| transition: opacity .2s !important;
|
| }
|
| button.primary:hover { opacity: .85 !important; }
|
|
|
| button.secondary {
|
| background: transparent !important;
|
| color: var(--accent) !important;
|
| border: 1px solid var(--accent) !important;
|
| font-family: 'IBM Plex Mono', monospace !important;
|
| font-size: 12px !important;
|
| border-radius: 6px !important;
|
| }
|
|
|
| /* Chatbot */
|
| .chatbot .message.user { background: var(--accent-dim) !important; border: 1px solid var(--accent) !important; }
|
| .chatbot .message.bot { background: var(--bg-surface) !important; border: 1px solid var(--border) !important; }
|
|
|
| footer { display: none !important; }
|
| """
|
|
|
|
|
|
|
|
|
|
|
|
|
| def _msg(role: str, content: str) -> dict:
|
| return {"role": role, "content": str(content)}
|
|
|
|
|
| def append_msgs(history: list, user_text: str, bot_text: str) -> list:
|
| """Append a user+assistant exchange to chat history."""
|
| return history + [_msg("user", user_text), _msg("assistant", bot_text)]
|
|
|
|
|
| def empty_history() -> list:
|
| return []
|
|
|
|
|
|
|
|
|
|
|
| def log_error(msg: str, ctx: str = "") -> None:
|
| ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| with open("error.txt", "a", encoding="utf-8") as f:
|
| f.write(f"\n{'='*60}\nTIME: {ts}\nCONTEXT: {ctx}\n"
|
| f"ERROR: {msg}\nTRACEBACK:\n{traceback.format_exc()}\n")
|
|
|
| try:
|
| print(f"[ERROR] {ctx}: {str(msg)[:120]}")
|
| except UnicodeEncodeError:
|
| print(f"[ERROR] {ctx}: (non-ASCII chars in message β see error.txt)")
|
|
|
|
|
| def safe_str(val) -> str:
|
| """Convert any LangGraph output to plain str safely."""
|
| if val is None:
|
| return ""
|
| if isinstance(val, str):
|
| return val
|
| if isinstance(val, list):
|
| parts = []
|
| for item in val:
|
| if isinstance(item, str):
|
| parts.append(item)
|
| elif isinstance(item, dict):
|
| parts.append(str(item.get("content", item.get("text", ""))))
|
| elif hasattr(item, "content"):
|
| parts.append(safe_str(item.content))
|
| else:
|
| parts.append(str(item))
|
| return "\n".join(filter(None, parts))
|
| if isinstance(val, dict):
|
| return str(val.get("content", val.get("text", str(val))))
|
| if hasattr(val, "content"):
|
| return safe_str(val.content)
|
| return str(val)
|
|
|
|
|
| def detect_phase_status() -> dict:
|
| return {
|
| "1": os.path.exists("loaded_data.csv"),
|
| "2": os.path.exists("labels_abstract.json") or os.path.exists("labels_title.json"),
|
| "3": os.path.exists("themes.json"),
|
| "4": os.path.exists("themes.json"),
|
| "5": os.path.exists("themes.json"),
|
| "5.5": os.path.exists("taxonomy_map.json"),
|
| "6": os.path.exists("narrative.txt"),
|
| }
|
|
|
|
|
| def build_phase_bar(status: dict) -> str:
|
| steps_html = ""
|
| for i, (key, label) in enumerate(PHASE_LABELS):
|
| done = status.get(key, False)
|
|
|
| earlier_all_done = all(status.get(PHASE_LABELS[j][0], False) for j in range(i))
|
| if done:
|
| state = "done"
|
| elif earlier_all_done:
|
| state = "active"
|
| else:
|
| state = "pending"
|
|
|
| icon = "β" if state == "done" else str(i + 1)
|
| connector = "" if i == len(PHASE_LABELS) - 1 else '<div class="phase-connector"></div>'
|
| steps_html += f"""
|
| <div class="phase-step {state}">
|
| <div class="phase-icon">{icon}</div>
|
| <div class="phase-label">{label}</div>
|
| </div>
|
| {connector}
|
| """
|
|
|
| return f"""
|
| <style>
|
| .phase-bar {{
|
| display: flex;
|
| align-items: center;
|
| padding: 18px 24px;
|
| background: linear-gradient(135deg, #0f1117 0%, #1a1d2e 100%);
|
| border: 1px solid #2a2d3e;
|
| border-radius: 12px;
|
| gap: 0;
|
| overflow-x: auto;
|
| font-family: 'IBM Plex Mono', monospace;
|
| }}
|
| .phase-step {{
|
| display: flex;
|
| flex-direction: column;
|
| align-items: center;
|
| gap: 6px;
|
| min-width: 72px;
|
| }}
|
| .phase-icon {{
|
| width: 32px;
|
| height: 32px;
|
| border-radius: 50%;
|
| display: flex;
|
| align-items: center;
|
| justify-content: center;
|
| font-size: 12px;
|
| font-weight: 700;
|
| transition: all 0.3s ease;
|
| }}
|
| .phase-step.done .phase-icon {{ background: #00e5a0; color: #000; }}
|
| .phase-step.active .phase-icon {{ background: #4f8ef7; color: #fff; box-shadow: 0 0 12px #4f8ef788; }}
|
| .phase-step.pending .phase-icon {{ background: #2a2d3e; color: #555; }}
|
| .phase-label {{
|
| font-size: 10px;
|
| letter-spacing: 0.05em;
|
| text-transform: uppercase;
|
| }}
|
| .phase-step.done .phase-label {{ color: #00e5a0; }}
|
| .phase-step.active .phase-label {{ color: #4f8ef7; }}
|
| .phase-step.pending .phase-label {{ color: #555; }}
|
| .phase-connector {{
|
| flex: 1;
|
| height: 2px;
|
| min-width: 20px;
|
| margin-bottom: 18px;
|
| background: #2a2d3e;
|
| }}
|
| </style>
|
| <div class="phase-bar">{steps_html}</div>
|
| """
|
|
|
|
|
| def parse_phase_status(text, current: dict) -> dict:
|
| text = safe_str(text)
|
| updated = dict(current)
|
| for line in text.splitlines():
|
| if "PHASE_STATUS:" in line:
|
| raw = line.split("PHASE_STATUS:", 1)[1].strip()
|
| for part in [p.strip() for p in raw.split(",")]:
|
| if "=" in part:
|
| k, v = part.split("=", 1)
|
| updated[k.strip()] = "β
" in v
|
| for k, v in detect_phase_status().items():
|
| updated[k] = updated.get(k, False) or v
|
| return updated
|
|
|
|
|
|
|
|
|
|
|
| def load_review_table() -> pd.DataFrame:
|
| if os.path.exists("taxonomy_map.json"):
|
| data = json.loads(open("taxonomy_map.json", encoding="utf-8").read())
|
| rows = []
|
| for i, item in enumerate(data):
|
| evidence = (
|
| f"β NOVEL | {item.get('reasoning','')[:80]}"
|
| if item.get("is_novel", False)
|
| else f"β PAJAIS: {item.get('pajais_match','')} | {item.get('reasoning','')[:60]}"
|
| )
|
| rows.append({"#": i, "Topic Label": item.get("theme_name", ""),
|
| "Top Evidence Sentence": evidence,
|
| "Sent.": 0, "Papers": 0, "Approve": True, "Rename To": ""})
|
| return pd.DataFrame(rows, columns=REVIEW_COLUMNS) if rows else EMPTY_REVIEW_DF
|
|
|
| if os.path.exists("themes.json"):
|
| data = json.loads(open("themes.json", encoding="utf-8").read())
|
| rows = []
|
| for i, item in enumerate(data):
|
| s = item.get("total_sentences", 0)
|
| rows.append({"#": i, "Topic Label": item.get("theme_name", ""),
|
| "Top Evidence Sentence": (
|
| item.get("representative_sentences", [""])[0][:120]
|
| if item.get("representative_sentences") else ""),
|
| "Sent.": s, "Papers": max(1, s // 10),
|
| "Approve": False, "Rename To": ""})
|
| return pd.DataFrame(rows, columns=REVIEW_COLUMNS) if rows else EMPTY_REVIEW_DF
|
|
|
| for rk in ("abstract", "title"):
|
| p = f"labels_{rk}.json"
|
| if os.path.exists(p):
|
| data = json.loads(open(p, encoding="utf-8").read())
|
| rows = []
|
| for t in data:
|
| s = t.get("count", 0)
|
| rows.append({"#": t.get("topic_id", 0),
|
| "Topic Label": t.get("label", f"Topic {t.get('topic_id',0)}"),
|
| "Top Evidence Sentence": (
|
| t.get("nearest_sentences", [""])[0][:120]
|
| if t.get("nearest_sentences") else ""),
|
| "Sent.": s, "Papers": max(1, s // 10),
|
| "Approve": False, "Rename To": ""})
|
| return pd.DataFrame(rows, columns=REVIEW_COLUMNS) if rows else EMPTY_REVIEW_DF
|
|
|
| return EMPTY_REVIEW_DF
|
|
|
|
|
| def get_downloads():
|
| found = [f for f in DOWNLOAD_FILES if os.path.exists(f)]
|
| return found if found else None
|
|
|
|
|
| def render_chart(chart_file: str) -> str:
|
| if not chart_file or not os.path.exists(chart_file):
|
| return ("<div style='padding:40px;text-align:center;color:#555;'>"
|
| "Chart not available yet β run analysis first.</div>")
|
| content = open(chart_file, encoding="utf-8").read()
|
| escaped = content.replace("&", "&").replace('"', """).replace("'", "'")
|
| return (f'<iframe srcdoc="{escaped}" style="width:100%;height:540px;'
|
| f'border:none;border-radius:6px;" '
|
| f'sandbox="allow-scripts allow-same-origin"></iframe>')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def call_agent(message: str, session_id: str, max_retries: int = 3) -> tuple[str, str]:
|
| """
|
| Invoke the LangGraph agent.
|
| Returns (response_text, session_id_used).
|
| session_id_used may differ from the input session_id if history corruption
|
| forced a thread rotation (FIX-A).
|
| """
|
| current_sid = session_id
|
|
|
| for attempt in range(max_retries):
|
| try:
|
| config = {"configurable": {"thread_id": current_sid}}
|
| result = agent.invoke(
|
| {"messages": [{"role": "user", "content": message}]},
|
| config=config,
|
| )
|
| for msg in reversed(result.get("messages", [])):
|
| if hasattr(msg, "type") and msg.type == "ai":
|
| return safe_str(msg.content), current_sid
|
| if isinstance(msg, dict) and msg.get("role") in ("assistant", "ai"):
|
| return safe_str(msg.get("content", "")), current_sid
|
| return "Agent returned no response. Please try again.", current_sid
|
|
|
| except Exception as e:
|
| err = str(e)
|
|
|
|
|
|
|
| if any(sig in err for sig in CORRUPT_HISTORY_SIGNALS):
|
| new_sid = str(uuid.uuid4())
|
| log_error(err, ctx=f"call_agent [corrupt-history β rotating {current_sid[:8]}β{new_sid[:8]}]")
|
| print(f"β οΈ Corrupt history detected β rotating session {current_sid[:8]} β {new_sid[:8]}")
|
| recovery_msg = (
|
| f"{message}\n\n"
|
| "[SYSTEM NOTE: The previous session thread had a corrupted history "
|
| "due to a mid-tool API failure. This is a fresh thread. "
|
| "Checkpoint files (themes.json, taxonomy_map.json, etc.) are intact on disk. "
|
| "Please resume from where we left off based on the existing checkpoint files.]"
|
| )
|
| current_sid = new_sid
|
|
|
| try:
|
| config = {"configurable": {"thread_id": current_sid}}
|
| result = agent.invoke(
|
| {"messages": [{"role": "user", "content": recovery_msg}]},
|
| config=config,
|
| )
|
| for msg in reversed(result.get("messages", [])):
|
| if hasattr(msg, "type") and msg.type == "ai":
|
| return safe_str(msg.content), current_sid
|
| if isinstance(msg, dict) and msg.get("role") in ("assistant", "ai"):
|
| return safe_str(msg.get("content", "")), current_sid
|
| return "Agent returned no response after history rotation. Please try again.", current_sid
|
| except Exception as e2:
|
| log_error(str(e2), ctx="call_agent [post-rotation]")
|
| return f"β οΈ Agent Error after session rotation: {e2}\n\nSee error.txt for details.", current_sid
|
|
|
|
|
| if any(c in err for c in ["429", "520", "502", "503", "529", "mistral.ai", "Rate limit"]):
|
| log_error(err, ctx=f"call_agent attempt {attempt + 1}")
|
| wait = 30 * (attempt + 1)
|
| print(f"β οΈ Mistral rate-limit/server error β retrying in {wait}sβ¦")
|
| time.sleep(wait)
|
| continue
|
|
|
| log_error(err, ctx="call_agent")
|
| return f"β οΈ Agent Error: {err}\n\nSee error.txt for details.", current_sid
|
|
|
| return "β Mistral not responding after retries. Wait a few minutes and try again.", current_sid
|
|
|
|
|
|
|
|
|
|
|
| def on_upload(file_obj, history, sid, status):
|
| if file_obj is None:
|
| return history, sid, status, build_phase_bar(status), load_review_table(), get_downloads()
|
| try:
|
| path = file_obj.name if hasattr(file_obj, "name") else str(file_obj)
|
| msg = (
|
| f"I have uploaded my Scopus CSV. File path: {path}\n\n"
|
| "Please begin Phase 1: load the file, show all dataset statistics "
|
| "(papers, abstract sentences, title sentences, year range, columns, "
|
| "sample titles), then ask me which run_key to use."
|
| )
|
| response, new_sid = call_agent(msg, sid)
|
| new_hist = append_msgs(history, msg, response)
|
| new_status = parse_phase_status(response, status)
|
| return new_hist, new_sid, new_status, build_phase_bar(new_status), load_review_table(), get_downloads()
|
| except Exception as e:
|
| log_error(str(e), ctx="on_upload")
|
| return (append_msgs(history, "[File Upload]", f"Upload error: {e}"),
|
| sid, status, build_phase_bar(status), load_review_table(), get_downloads())
|
|
|
|
|
| def on_send(user_msg, history, sid, status):
|
| if not user_msg.strip():
|
| return history, "", sid, status, build_phase_bar(status), load_review_table(), get_downloads()
|
| try:
|
| response, new_sid = call_agent(user_msg, sid)
|
| new_hist = append_msgs(history, user_msg, response)
|
| new_status = parse_phase_status(response, status)
|
| return new_hist, "", new_sid, new_status, build_phase_bar(new_status), load_review_table(), get_downloads()
|
| except Exception as e:
|
| log_error(str(e), ctx="on_send")
|
| return (append_msgs(history, user_msg, f"Error: {e}"),
|
| "", sid, status, build_phase_bar(status), load_review_table(), get_downloads())
|
|
|
|
|
| def on_submit_review(review_df, history, sid, status):
|
| try:
|
| df = review_df if isinstance(review_df, pd.DataFrame) else pd.DataFrame(review_df)
|
| approved = df[df["Approve"].astype(bool)]
|
| rename_map = {}
|
| labels_list = []
|
|
|
| for _, row in approved.iterrows():
|
| tid = str(row.get("#", ""))
|
| label = str(row.get("Topic Label", "")).strip()
|
| ren = str(row.get("Rename To", "")).strip()
|
| labels_list.append(ren if ren else label)
|
| if ren:
|
| rename_map[tid] = ren
|
|
|
| lines = []
|
| if labels_list:
|
| shown = ", ".join(labels_list[:6]) + ("β¦" if len(labels_list) > 6 else "")
|
| lines.append(f"Approved {len(labels_list)} row(s): {shown}")
|
| if rename_map:
|
| lines.append("Renames: " + ", ".join(
|
| f"#{k}β'{v}'" for k, v in list(rename_map.items())[:5]))
|
| summary = "\n".join(lines) if lines else "No approvals or renames submitted."
|
|
|
| msg = (
|
| "I have submitted the Review Table.\n\n"
|
| f"Decisions:\n{summary}\n\n"
|
| f"Rename overrides JSON: {json.dumps(rename_map)}\n\n"
|
| "Please proceed to the next phase using these decisions."
|
| )
|
| response, new_sid = call_agent(msg, sid)
|
| new_hist = append_msgs(history, msg, response)
|
| new_status = parse_phase_status(response, status)
|
| return new_hist, new_sid, new_status, build_phase_bar(new_status), load_review_table(), get_downloads()
|
| except Exception as e:
|
| log_error(str(e), ctx="on_submit_review")
|
| return (append_msgs(history, "[Submit Review]", f"Submit error: {e}"),
|
| sid, status, build_phase_bar(status), load_review_table(), get_downloads())
|
|
|
|
|
| def on_chart_change(label: str) -> str:
|
| return render_chart(dict(CHART_OPTIONS).get(label, ""))
|
|
|
|
|
| def on_clear(sid):
|
| """Reset the UI and wipe all checkpoint files so Phase 1 re-runs clean."""
|
| for f in CHECKPOINT_FILES:
|
| if os.path.exists(f):
|
| try:
|
| os.remove(f)
|
| except OSError:
|
| pass
|
| new_sid = str(uuid.uuid4())
|
| blank = {k: False for k in ["1", "2", "3", "4", "5", "5.5", "6"]}
|
| new_status = parse_phase_status("", blank)
|
| return empty_history(), new_sid, new_status, build_phase_bar(new_status)
|
|
|
|
|
|
|
|
|
|
|
| INIT_STATUS = parse_phase_status("", {k: False for k in ["1","2","3","4","5","5.5","6"]})
|
|
|
| with gr.Blocks(title="BERTopic Agentic Topic Modelling") as demo:
|
|
|
|
|
| sid_state = gr.State(str(uuid.uuid4()))
|
| history_state = gr.State(empty_history())
|
| status_state = gr.State(INIT_STATUS)
|
|
|
|
|
| gr.HTML("""
|
| <div style="
|
| padding: 28px 0 20px;
|
| font-family: 'IBM Plex Mono', monospace;
|
| ">
|
| <div style="font-size:22px;font-weight:700;color:#d4d8f0;letter-spacing:0.04em;">
|
| BERTopic <span style="color:#4f8ef7;">Agent</span>
|
| </div>
|
| <div style="font-size:11px;color:#666a88;margin-top:4px;letter-spacing:0.12em;text-transform:uppercase;">
|
| Agentic Topic Modelling Β· Braun & Clarke 2006
|
| </div>
|
| </div>
|
| """)
|
|
|
|
|
|
|
|
|
| with gr.Group():
|
| gr.HTML('<div class="section-header">01 β Data Input</div>')
|
|
|
| file_input = gr.File(
|
| label="Upload Scopus CSV",
|
| file_types=[".csv"],
|
| height=100,
|
| )
|
| gr.HTML("<p style='color:#666a88;font-size:0.73rem;margin:4px 2px;font-family:monospace;'>"
|
| "Upload CSV β auto-triggers Phase 1</p>")
|
|
|
| phase_bar = gr.HTML(value=build_phase_bar(INIT_STATUS))
|
|
|
|
|
|
|
|
|
| with gr.Group():
|
| gr.HTML('<div class="section-header">02 β Agent Conversation</div>')
|
|
|
| chatbot = gr.Chatbot(
|
| value=empty_history(),
|
| height=400,
|
| show_label=False,
|
| avatar_images=(None, "https://api.dicebear.com/8.x/bottts/svg?seed=bertopic"),
|
| )
|
|
|
| with gr.Row():
|
| chat_input = gr.Textbox(
|
| show_label=False,
|
| placeholder="Type 'run abstract', 'Continue', or any messageβ¦",
|
| scale=8, lines=1, max_lines=4, container=False,
|
| )
|
| send_btn = gr.Button("Send β", variant="primary", scale=1, min_width=90)
|
|
|
| clear_btn = gr.Button("π Clear Chat & Reset", variant="secondary", size="sm")
|
|
|
|
|
|
|
|
|
| with gr.Group():
|
| gr.HTML('<div class="section-header">03 β Results</div>')
|
|
|
| with gr.Tabs():
|
|
|
|
|
| with gr.Tab("π Review Table"):
|
| review_table = gr.Dataframe(
|
| value=load_review_table(),
|
| headers=REVIEW_COLUMNS,
|
| datatype=["number", "str", "str", "number", "number", "bool", "str"],
|
| interactive=True,
|
| wrap=True,
|
| row_count=(6, "dynamic"),
|
| column_count=(7, "fixed"),
|
| show_label=False,
|
| )
|
| with gr.Row():
|
| submit_btn = gr.Button("Submit Review", variant="primary")
|
| gr.HTML("<p style='color:#666a88;font-size:0.73rem;margin:4px 2px;font-family:monospace;'>"
|
| "Tick Approve / fill Rename To, then click Submit Review.</p>")
|
|
|
|
|
| with gr.Tab("π Charts"):
|
| chart_dd = gr.Dropdown(
|
| choices=[o[0] for o in CHART_OPTIONS],
|
| value=CHART_OPTIONS[0][0],
|
| label="Select chart",
|
| interactive=True,
|
| )
|
| chart_display = gr.HTML(
|
| value="<p style='color:#666a88;padding:24px;font-family:monospace;'>Charts appear after Phase 2 completes.</p>"
|
| )
|
|
|
|
|
| with gr.Tab("β¬ Download"):
|
| gr.Markdown(
|
| "_Generated output files will appear below once the pipeline completes._",
|
| )
|
| dl_box = gr.File(
|
| value=get_downloads(),
|
| show_label=False,
|
| file_count="multiple",
|
| interactive=False,
|
| height=180,
|
| )
|
|
|
|
|
| file_input.change(
|
| fn=on_upload,
|
| inputs=[file_input, history_state, sid_state, status_state],
|
| outputs=[chatbot, sid_state, status_state, phase_bar, review_table, dl_box],
|
| )
|
| chatbot.change(fn=lambda h: h, inputs=chatbot, outputs=history_state)
|
|
|
| send_btn.click(
|
| fn=on_send,
|
| inputs=[chat_input, history_state, sid_state, status_state],
|
| outputs=[chatbot, chat_input, sid_state, status_state, phase_bar, review_table, dl_box],
|
| )
|
| chat_input.submit(
|
| fn=on_send,
|
| inputs=[chat_input, history_state, sid_state, status_state],
|
| outputs=[chatbot, chat_input, sid_state, status_state, phase_bar, review_table, dl_box],
|
| )
|
| submit_btn.click(
|
| fn=on_submit_review,
|
| inputs=[review_table, history_state, sid_state, status_state],
|
| outputs=[chatbot, sid_state, status_state, phase_bar, review_table, dl_box],
|
| )
|
| chart_dd.change(fn=on_chart_change, inputs=chart_dd, outputs=chart_display)
|
| clear_btn.click(
|
| fn=on_clear,
|
| inputs=[sid_state],
|
| outputs=[chatbot, sid_state, status_state, phase_bar],
|
| )
|
|
|
|
|
| if __name__ == "__main__":
|
| demo.launch(
|
| server_name="0.0.0.0",
|
| server_port=7860,
|
| show_error=True,
|
| css=CSS,
|
| ssr_mode=False,
|
| theme=gr.themes.Base(
|
| primary_hue="blue",
|
| neutral_hue="slate",
|
| ),
|
| ) |