milindkamat0507 commited on
Commit
6dced03
·
verified ·
1 Parent(s): 6b3ff64

Upload 2 files

Browse files
Files changed (2) hide show
  1. agent.py +159 -0
  2. app.py +326 -0
agent.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ agent.py — Braun & Clarke (2006) Thematic Analysis Agent.
3
+
4
+ 10 tools. 6 STOP gates. Reviewer approval after every interpretive output.
5
+ Every number comes from a tool — the LLM never computes values.
6
+ """
7
+
8
+ from langchain_mistralai import ChatMistralAI
9
+ from langchain.agents import create_agent
10
+ from langgraph.checkpoint.memory import InMemorySaver
11
+ from tools import ALL_TOOLS
12
+
13
+ SYSTEM_PROMPT = """
14
+ You are a Braun & Clarke (2006) Computational Thematic Analysis Agent.
15
+
16
+ RULES:
17
+ 1. ONE PHASE PER MESSAGE — STRICTLY ENFORCED.
18
+ After calling a tool, IMMEDIATELY present results and STOP.
19
+ Do NOT call a second tool in the same message.
20
+ Do NOT skip ahead to the next phase.
21
+ Do NOT combine phases.
22
+ The sequence MUST be: call tool → summarise result → STOP → wait.
23
+ Example CORRECT flow:
24
+ Message 1: Call load_scopus_csv → "Loaded 1,390 papers" → STOP
25
+ Message 2: Call run_bertopic_discovery → "Found 98 clusters" → STOP
26
+ Message 3: Call label_topics_with_llm → "Labelled 98 clusters" → STOP
27
+ Example WRONG flow:
28
+ Message 1: Call load_scopus_csv → call run_bertopic_discovery →
29
+ call label_topics_with_llm → "All done!" ← NEVER DO THIS
30
+ 2. ALL APPROVALS VIA REVIEW TABLE — never via chat. When review needed:
31
+ [WAITING FOR REVIEW TABLE]
32
+ Edit Approve / Rename To / Move To / Reasoning, then Submit Review.
33
+ 3. NEVER FABRICATE DATA — every number, percentage, score, sentence list
34
+ MUST come from a tool. You CANNOT do arithmetic. If you need a number,
35
+ call a tool. If no tool exists for what you need, say so.
36
+ 4. STOP GATES ARE ABSOLUTE — [FAILED] halts unconditionally.
37
+ 5. EMIT PHASE STATUS at top of every response:
38
+ "[Phase X/6 | STOP Gates Passed: N/6 | Pending Review: Yes/No]"
39
+ 6. TOOL ERRORS: log verbatim, identify cause, propose fix, wait.
40
+ 7. AUTHOR KEYWORDS EXCLUDED from all embedding and clustering.
41
+ 8. CHAT IS CONVERSATION, NOT DATA DUMP.
42
+ Your response in the chat window must be SHORT and CONVERSATIONAL:
43
+ - 3-5 sentences maximum summarising what you did
44
+ - State key numbers: "Found 45 clusters, 12 orphans"
45
+ - NEVER put markdown tables, JSON, raw data, or long lists in chat
46
+ - NEVER repeat the full tool output in chat
47
+ The Review Table (Section 3) auto-populates from your tool's
48
+ checkpoint files. The user sees the data THERE, not in chat.
49
+
50
+ REVIEW TABLE STATUS — say the right thing for the right phase:
51
+ - PHASE 1 (load_scopus_csv): NO review table data exists yet.
52
+ End with: "Type 'run abstract' or 'run title' to proceed to
53
+ BERTopic discovery." Do NOT say "Results in Review Table."
54
+ - PHASE 2+ (after run_bertopic_discovery, label_topics_with_llm,
55
+ consolidate_into_themes, etc.): Review table IS populated.
56
+ End with: "Results are loaded in the Review Table below.
57
+ Please review and click Submit Review when ready."
58
+ The rule: only mention the Review Table if your tool actually
59
+ wrote a JSON checkpoint file (topic_labels.json, themes.json,
60
+ summaries.json, taxonomy_alignment.json) that the table can load.
61
+
62
+ 10 TOOLS:
63
+ DETERMINISTIC (same input → same output):
64
+ 1. load_scopus_csv — Phase 1: clean CSV, count, save .parquet
65
+ 2. run_bertopic_discovery — Phase 2: embed + cluster (min 3 members)
66
+ + orphan report + 4 charts
67
+ 4. reassign_sentences — Phase 2: move orphans/sentences between clusters
68
+ 5. consolidate_into_themes — Phase 3: merge groups, recompute centroids
69
+ 6. compute_saturation — Phase 4: coverage %, coherence, balance
70
+ 7. generate_theme_profiles — Phase 5: top 5 nearest sentences per theme
71
+ 9. generate_comparison_csv — Phase 6: abstract vs title joined on PAJAIS
72
+
73
+ LLM-DEPENDENT (grounded in real data, reviewer must approve):
74
+ 3. label_topics_with_llm — Phase 2: Mistral names clusters
75
+ 8. compare_with_taxonomy — Phase 5.5: map themes to PAJAIS 25
76
+ 10. export_narrative — Phase 6: 500-word Section 7
77
+
78
+ B&C 6-PHASE METHODOLOGY:
79
+
80
+ PHASE 1 — FAMILIARISATION
81
+ The user message may contain a [CSV: /path/to/file.csv] prefix.
82
+ Extract the FULL path (everything between "CSV: " and "]") and pass
83
+ it as csv_path to load_scopus_csv. Do NOT modify or shorten the path.
84
+ Call load_scopus_csv. Show stats. STOP. Wait for "run abstract"/"run title".
85
+
86
+ PHASE 2 — INITIAL CODES (3 separate messages, one tool each)
87
+ MESSAGE 1: Call run_bertopic_discovery. Report: total clusters, orphan count.
88
+ Say "Results loaded in the Review Table below." STOP. Wait.
89
+ MESSAGE 2 (after user says proceed): Call label_topics_with_llm.
90
+ Report: how many labelled. Say "Labels loaded in Review Table." STOP.
91
+ If orphans > 0, tell reviewer: "N sentences did not fit any cluster
92
+ (minimum 3 members required). Use Move To column to reassign."
93
+ STOP GATE 1: SG1-A (<5 topics), SG1-B (confidence <0.40),
94
+ SG1-C (>40% generic), SG1-D (duplicates).
95
+ [WAITING FOR REVIEW TABLE]. STOP.
96
+ MESSAGE 3 (after Submit Review): if moves exist, call reassign_sentences.
97
+
98
+ PHASE 3 — THEMES
99
+ Parse review. Call consolidate_into_themes.
100
+ STOP GATE 2: SG2-A (<3 themes), SG2-B (singleton),
101
+ SG2-C (duplicates), SG2-D (coverage <50%).
102
+ [WAITING FOR REVIEW TABLE]. STOP.
103
+
104
+ PHASE 4 — SATURATION
105
+ Call compute_saturation (NEVER compute these numbers yourself).
106
+ Present the EXACT numbers returned by the tool.
107
+ STOP GATE 3: SG3-A (coverage <60%), SG3-B (single theme >60%),
108
+ SG3-C (coherence <0.30), SG3-D (<3 themes).
109
+ [WAITING FOR REVIEW TABLE]. STOP.
110
+
111
+ PHASE 5 — NAMING
112
+ Call generate_theme_profiles (NEVER recall sentences from memory).
113
+ Present the EXACT top-5 sentences returned by the tool per theme.
114
+ Propose names based on these real sentences.
115
+ [WAITING FOR REVIEW TABLE]. STOP.
116
+
117
+ PHASE 5.5 — PAJAIS MAPPING
118
+ Call compare_with_taxonomy.
119
+ STOP GATE 4: SG4-A (zero categories), SG4-B (>30% score <0.40),
120
+ SG4-C (single category >50%), SG4-D (incomplete).
121
+ [WAITING FOR REVIEW TABLE]. STOP.
122
+
123
+ PHASE 6 — REPORT
124
+ Call generate_comparison_csv. Present convergence/divergence summary.
125
+ STOP GATE 5: Reviewer confirms comparison makes sense.
126
+ [WAITING FOR REVIEW TABLE]. STOP.
127
+ Call export_narrative. Present full 500-word draft.
128
+ STOP GATE 6: Reviewer approves final narrative.
129
+ [WAITING FOR REVIEW TABLE]. STOP.
130
+ DONE — all 6 gates passed.
131
+
132
+ 6 STOP GATES:
133
+ STOP-1 (Phase 2) : Initial Code Quality
134
+ STOP-2 (Phase 3) : Theme Coherence
135
+ STOP-3 (Phase 4) : Saturation Adequacy
136
+ STOP-4 (Phase 5.5) : Taxonomy Alignment Quality
137
+ STOP-5 (Phase 6) : Comparison Review [NEW]
138
+ STOP-6 (Phase 6) : Narrative Approval [NEW]
139
+ """
140
+
141
+ llm = ChatMistralAI(model="mistral-large-latest", temperature=0, max_tokens=8192)
142
+
143
+ memory = InMemorySaver()
144
+
145
+ agent = create_agent(
146
+ model=llm,
147
+ tools=ALL_TOOLS,
148
+ system_prompt=SYSTEM_PROMPT,
149
+ checkpointer=memory,
150
+ )
151
+
152
+
153
+ def run(user_message: str, thread_id: str = "default") -> str:
154
+ """Invoke the agent for one conversation turn."""
155
+ config = {"configurable": {"thread_id": thread_id}}
156
+ payload = {"messages": [{"role": "user", "content": user_message}]}
157
+ result = agent.invoke(payload, config=config)
158
+ msgs = result.get("messages", [])
159
+ return (msgs and msgs[-1].content) or ""
app.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ app.py — BERTopic Topic Modelling Agent UI.
3
+
4
+ Three UX features:
5
+ 1. Phase banner — large prominent display of current B&C phase
6
+ 2. Dynamic prompts — phase-appropriate suggested next actions
7
+ 3. Auto-populated review table — loads from tool checkpoint files
8
+
9
+ 9-column review table: #, Topic Label, Top Evidence, Sentences, Papers,
10
+ Approve, Rename To, Move To, Reasoning.
11
+ """
12
+
13
+ import gradio as gr
14
+ import pandas as pd
15
+ import json
16
+ import os
17
+ import re
18
+ import tempfile
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from agent import run as agent_run
22
+
23
+ THREAD_ID = f"bertopic-{datetime.now().strftime('%Y%m%d%H%M%S')}"
24
+
25
+ REVIEW_COLS = [
26
+ "#", "Topic Label", "Top Evidence", "Sentences", "Papers",
27
+ "Approve", "Rename To", "Move To", "Reasoning",
28
+ ]
29
+
30
+ EMPTY_TABLE = pd.DataFrame(
31
+ {"#": ["-"], "Topic Label": ["No results yet — run analysis first"],
32
+ "Top Evidence": [""], "Sentences": [""], "Papers": [""],
33
+ "Approve": [""], "Rename To": [""], "Move To": [""], "Reasoning": [""]},
34
+ )
35
+
36
+ PHASE_INFO = {
37
+ 0: ("Getting started", "⬜⬜⬜⬜⬜⬜",
38
+ "Upload a CSV file, then click **Analyze my Scopus CSV** and press Send"),
39
+ 1: ("Phase 1 — Familiarisation", "🟦⬜⬜⬜⬜⬜",
40
+ "Click **Run abstract analysis** or **Run title analysis** and press Send"),
41
+ 2: ("Phase 2 — Initial Codes", "🟦🟦⬜⬜⬜⬜",
42
+ "Review clusters in the **Results table** below. Edit Approve / Rename / Move, "
43
+ "then click **Submit Review**"),
44
+ 3: ("Phase 3 — Themes", "🟦🟦🟦⬜⬜⬜",
45
+ "Review merged themes. Edit the table, then click **Submit Review**"),
46
+ 4: ("Phase 4 — Saturation", "🟦🟦🟦🟦⬜⬜",
47
+ "Review saturation metrics. Click **Submit Review** to confirm"),
48
+ 5: ("Phase 5 — Naming", "🟦🟦🟦🟦🟦⬜",
49
+ "Review theme profiles. Edit names, then **Submit Review**"),
50
+ 6: ("Phase 6 — Report", "🟦🟦🟦🟦🟦🟦",
51
+ "Review comparison and narrative. **Submit Review** to finalise"),
52
+ }
53
+
54
+ _path = lambda file: str(
55
+ (hasattr(file, "name") and file.name)
56
+ or (isinstance(file, str) and file)
57
+ or ""
58
+ )
59
+ _name = lambda file: os.path.basename(_path(file))
60
+
61
+
62
+ def _extract_phase(text: str) -> int:
63
+ """Extract phase number from agent response. Returns 0 if not found."""
64
+ found = re.findall(r"Phase (\d)", str(text))
65
+ return int((found or ["0"])[0])
66
+
67
+
68
+ def _phase_banner(num: int) -> str:
69
+ """Generate prominent phase banner with progress bar and next step."""
70
+ name, progress, instruction = PHASE_INFO.get(num, PHASE_INFO[0])
71
+ return (
72
+ f"## {progress} {name}\n\n"
73
+ f"**NEXT STEP →** {instruction}"
74
+ )
75
+
76
+
77
+ def _load_review_table(base_dir: str) -> pd.DataFrame:
78
+ """Load latest checkpoint file into the 9-column review table.
79
+
80
+ Scans base_dir for topic_labels.json, themes.json, taxonomy_alignment.json,
81
+ summaries.json. Loads the most recently modified one and formats it.
82
+ Returns EMPTY_TABLE if nothing found.
83
+ """
84
+ base = Path(str(base_dir or "/tmp/nonexistent_dir_placeholder"))
85
+ candidates = (
86
+ base_dir and base.exists() and sorted(
87
+ (
88
+ list(base.glob("topic_labels.json"))
89
+ + list(base.glob("themes.json"))
90
+ + list(base.glob("taxonomy_alignment.json"))
91
+ + list(base.glob("summaries.json"))
92
+ ),
93
+ key=lambda p: p.stat().st_mtime,
94
+ reverse=True,
95
+ )
96
+ ) or []
97
+
98
+ latest = (candidates[:1] or [None])[0]
99
+ return (latest and [_format_checkpoint(latest)] or [EMPTY_TABLE.copy()])[0]
100
+
101
+
102
+ def _format_checkpoint(path) -> pd.DataFrame:
103
+ """Format a checkpoint JSON file into review table rows.
104
+
105
+ Merges data from multiple checkpoint files when available:
106
+ topic_labels.json has labels but no sizes — summaries.json has sizes.
107
+ """
108
+ raw = json.loads(Path(path).read_text())
109
+ base = Path(path).parent
110
+
111
+ data = (isinstance(raw, dict) and raw.get("clusters", raw.get("per_theme", []))) or \
112
+ (isinstance(raw, list) and raw) or []
113
+
114
+ summaries_data = {}
115
+ summaries_path = base / "summaries.json"
116
+ summaries_raw = (
117
+ summaries_path.exists() and json.loads(summaries_path.read_text()) or {}
118
+ )
119
+ summaries_list = (
120
+ isinstance(summaries_raw, dict) and summaries_raw.get("clusters", [])
121
+ ) or (isinstance(summaries_raw, list) and summaries_raw) or []
122
+ list(map(
123
+ lambda s: summaries_data.update({s.get("topic_id", -999): s}),
124
+ summaries_list,
125
+ ))
126
+
127
+ def _row(item: dict) -> dict:
128
+ """Map one JSON item to review table columns, merging summaries data."""
129
+ tid = item.get("topic_id", item.get("theme_id", 0))
130
+ summary = summaries_data.get(tid, {})
131
+ return {
132
+ "#": tid,
133
+ "Topic Label": item.get("label", item.get("theme_label", "")),
134
+ "Top Evidence": str(
135
+ item.get("representative", "")
136
+ or summary.get("representative", "")
137
+ or item.get("notes", "")
138
+ )[:150],
139
+ "Sentences": item.get("size", 0) or summary.get("size", 0)
140
+ or item.get("total_papers", 0),
141
+ "Papers": item.get("size", 0) or summary.get("size", 0)
142
+ or item.get("total_papers", 0),
143
+ "Approve": "Yes",
144
+ "Rename To": "",
145
+ "Move To": "",
146
+ "Reasoning": str(item.get("rationale", item.get("notes", ""))),
147
+ }
148
+
149
+ rows = list(map(_row, data[:200]))
150
+ return (rows and [pd.DataFrame(rows, columns=REVIEW_COLS)] or [EMPTY_TABLE.copy()])[0]
151
+
152
+
153
+ def on_file_upload(file):
154
+ """Extract CSV stats and store base directory."""
155
+ path = _path(file)
156
+ result = (not path) and ("Upload a CSV to begin.", "", _phase_banner(0))
157
+ return result or _do_file_upload(path, file)
158
+
159
+
160
+ def _do_file_upload(path: str, file) -> tuple:
161
+ """Actual file processing after path validation."""
162
+ df = pd.read_csv(path)
163
+ rows, cols = df.shape
164
+ base = str(Path(path).parent)
165
+ info = (
166
+ f"**Loaded:** `{_name(file)}`\n\n"
167
+ f"**Shape:** {rows:,} rows x {cols} columns\n\n"
168
+ f"**Columns:** {', '.join(df.columns[:6].tolist())}\n\n"
169
+ f"*Click a prompt below and press Send to begin.*"
170
+ )
171
+ return info, base, _phase_banner(1)
172
+
173
+
174
+ def on_send(user_msg, history, file, base_dir):
175
+ """Pass user message to agent. Update phase banner and review table."""
176
+ msg = (user_msg or "").strip() or "help"
177
+ csv_tag = f"[CSV: {_path(file)}]\n" * bool(file)
178
+
179
+ history = list(history or [])
180
+ history.append({"role": "user", "content": msg})
181
+ history.append({"role": "assistant", "content": "Thinking..."})
182
+ yield history, "", gr.skip(), gr.skip(), gr.skip()
183
+
184
+ reply = agent_run(csv_tag + msg, thread_id=THREAD_ID)
185
+ history[-1] = {"role": "assistant", "content": reply}
186
+
187
+ phase = _extract_phase(reply)
188
+ banner = _phase_banner(phase)
189
+ table = _load_review_table(base_dir)
190
+
191
+ yield history, "", banner, table, base_dir
192
+
193
+
194
+ def on_submit_review(table_df, history, base_dir):
195
+ """Serialise review table edits to agent."""
196
+ history = list(history or [])
197
+ edits = table_df.to_json(orient="records", indent=2)
198
+
199
+ history.append({"role": "user", "content": "[REVIEW SUBMITTED]"})
200
+ history.append({"role": "assistant", "content": "Processing review..."})
201
+
202
+ reply = agent_run(
203
+ "Reviewer submitted table edits.\n\n"
204
+ f"```json\n{edits}\n```\n\n"
205
+ "Process: Approve/Reject decisions, Rename To values, "
206
+ "Move To reassignments (call reassign_sentences if moves exist), "
207
+ "Reasoning notes. Then check STOP gates and proceed.",
208
+ thread_id=THREAD_ID,
209
+ )
210
+ history[-1] = {"role": "assistant", "content": reply}
211
+
212
+ phase = _extract_phase(reply)
213
+ return history, _phase_banner(phase), _load_review_table(base_dir)
214
+
215
+
216
+ def on_download(table_df, history):
217
+ """Export review CSV and chat TXT."""
218
+ csv_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="review_")
219
+ table_df.to_csv(csv_tmp.name, index=False)
220
+
221
+ txt_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", prefix="chat_")
222
+ txt_tmp.write(
223
+ "\n\n".join(
224
+ list(map(
225
+ lambda m: f"{m.get('role', '').upper()}: {m.get('content', '')}",
226
+ history or [],
227
+ ))
228
+ ).encode("utf-8")
229
+ )
230
+ txt_tmp.close()
231
+ return [csv_tmp.name, txt_tmp.name]
232
+
233
+
234
+ with gr.Blocks(title="BERTopic Agent") as demo:
235
+
236
+ base_dir_state = gr.State(value="")
237
+
238
+ gr.Markdown("# BERTopic Modelling Agent")
239
+ gr.Markdown(
240
+ "**Braun & Clarke 6-Phase Thematic Analysis** "
241
+ "| 10 Tools | 6 STOP Gates | Cosine Agglomerative Clustering"
242
+ )
243
+
244
+ phase_banner = gr.Markdown(value=_phase_banner(0))
245
+
246
+ gr.Markdown("---\n### Section 1 — Data input")
247
+ with gr.Row():
248
+ with gr.Column(scale=3):
249
+ file_input = gr.File(
250
+ label="Upload Scopus CSV",
251
+ file_types=[".csv"],
252
+ file_count="single",
253
+ )
254
+ with gr.Column(scale=5):
255
+ file_info = gr.Markdown("Upload a CSV to begin.")
256
+
257
+ gr.Markdown("---\n### Section 2 — Agent conversation")
258
+ chatbot = gr.Chatbot(label="BERTopic Agent", height=200)
259
+ with gr.Row():
260
+ msg_box = gr.Textbox(
261
+ placeholder="Type a message or click a prompt below, then press Send",
262
+ show_label=False, scale=7, lines=1,
263
+ )
264
+ send_btn = gr.Button("Send", variant="primary", scale=1)
265
+
266
+ gr.Examples(
267
+ examples=[
268
+ "Analyze my Scopus CSV",
269
+ "Run abstract analysis",
270
+ "Run title analysis",
271
+ "Proceed to next phase",
272
+ "Show corpus statistics",
273
+ ],
274
+ inputs=msg_box,
275
+ label="Quick prompts (click to fill, then press Send)",
276
+ )
277
+
278
+ gr.Markdown("---\n### Section 3 — Results (auto-populated from tool outputs)")
279
+ gr.Markdown(
280
+ "This table fills automatically when the agent runs tools. "
281
+ "Edit **Approve**, **Rename To**, **Move To**, **Reasoning** columns, "
282
+ "then click **Submit Review**."
283
+ )
284
+ review_table = gr.Dataframe(
285
+ value=EMPTY_TABLE,
286
+ headers=REVIEW_COLS,
287
+ datatype=["number", "str", "str", "number", "number",
288
+ "str", "str", "str", "str"],
289
+ column_count=(9, "fixed"),
290
+ interactive=True,
291
+ wrap=True,
292
+ max_height=400,
293
+ )
294
+ with gr.Row():
295
+ clear_btn = gr.Button("Clear table", variant="secondary", scale=2)
296
+ sub_btn = gr.Button("Submit Review", variant="primary", scale=4)
297
+
298
+ with gr.Accordion("Download", open=False):
299
+ dl_btn = gr.Button("Generate downloads", variant="primary")
300
+ dl_files = gr.File(label="Downloads", file_count="multiple",
301
+ interactive=False)
302
+
303
+ file_input.change(
304
+ on_file_upload,
305
+ inputs=[file_input],
306
+ outputs=[file_info, base_dir_state, phase_banner],
307
+ )
308
+ send_btn.click(
309
+ on_send,
310
+ inputs=[msg_box, chatbot, file_input, base_dir_state],
311
+ outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state],
312
+ )
313
+ msg_box.submit(
314
+ on_send,
315
+ inputs=[msg_box, chatbot, file_input, base_dir_state],
316
+ outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state],
317
+ )
318
+ clear_btn.click(lambda: EMPTY_TABLE.copy(), outputs=[review_table])
319
+ sub_btn.click(
320
+ on_submit_review,
321
+ inputs=[review_table, chatbot, base_dir_state],
322
+ outputs=[chatbot, phase_banner, review_table],
323
+ )
324
+ dl_btn.click(on_download, inputs=[review_table, chatbot], outputs=[dl_files])
325
+
326
+ demo.launch(ssr_mode=False, theme=gr.themes.Soft())