milindkamat0507 commited on
Commit
d834a13
·
verified ·
1 Parent(s): 15fe1ba

Upload 2 files

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