milindkamat0507 commited on
Commit
d318a55
·
verified ·
1 Parent(s): 364a1be

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -548
app.py DELETED
@@ -1,548 +0,0 @@
1
- """
2
- app.py — Braun & Clarke (2006) Thematic Analysis Agent UI.
3
-
4
- Implements the 6-phase reflexive thematic analysis procedure from
5
- Braun, V., & Clarke, V. (2006). Using thematic analysis in psychology.
6
- Qualitative Research in Psychology, 3(2), 77-101.
7
-
8
- Three UX features:
9
- 1. Phase banner — large prominent display of current B&C phase
10
- 2. Dynamic phase actions — only actions valid for current phase shown
11
- 3. Auto-populated review table — loads from tool checkpoint files
12
-
13
- 9-column review table: #, Code/Theme Label, Data Extract, Extracts,
14
- Data Items, Approve, Rename To, Move To, Analytic Memo.
15
- """
16
-
17
- import gradio as gr
18
- import pandas as pd
19
- import json
20
- import os
21
- import re
22
- import tempfile
23
- from datetime import datetime
24
- from pathlib import Path
25
- from agent import run as agent_run
26
-
27
- THREAD_ID = f"thematic-analysis-{datetime.now().strftime('%Y%m%d%H%M%S')}"
28
-
29
- REVIEW_COLS = [
30
- "#", "Code / Theme Label", "Data Extract", "Extracts", "Data Items",
31
- "Approve", "Rename To", "Move To", "Analytic Memo",
32
- ]
33
-
34
- EMPTY_TABLE = pd.DataFrame(
35
- {"#": ["-"], "Code / Theme Label": ["No codes yet — run analysis first"],
36
- "Data Extract": [""], "Extracts": [""], "Data Items": [""],
37
- "Approve": [""], "Rename To": [""], "Move To": [""], "Analytic Memo": [""]},
38
- )
39
-
40
- PHASE_INFO = {
41
- 0: ("Getting started", "⬜⬜⬜⬜⬜⬜",
42
- "Upload your Scopus CSV data set, then click **Analyse my data set**"),
43
- 1: ("Phase 1 — Familiarisation with the Data", "🟦⬜⬜⬜⬜⬜",
44
- "Click **Run analysis on abstracts** or **Run analysis on titles** "
45
- "to begin familiarisation with the data corpus"),
46
- 2: ("Phase 2 — Generating Initial Codes", "🟦🟦⬜⬜⬜⬜",
47
- "Review initial codes in the table below. Edit Approve / Rename / "
48
- "Move extracts, then click **Submit Review** to collate codes into themes"),
49
- 3: ("Phase 3 — Searching for Themes", "🟦🟦🟦⬜⬜⬜",
50
- "Review candidate themes (collated initial codes). Edit the table "
51
- "and click **Submit Review** to proceed to theme review"),
52
- 4: ("Phase 4 — Reviewing Themes", "🟦🟦🟦🟦⬜⬜",
53
- "Review themes against coded extracts (Level 1) and the entire "
54
- "data set (Level 2). Click **Submit Review** to confirm"),
55
- 5: ("Phase 5 — Defining and Naming Themes", "🟦🟦🟦🟦🟦⬜",
56
- "Review theme definitions and names. Edit and click **Submit Review**"),
57
- 6: ("Phase 6 — Producing the Report", "🟦🟦🟦🟦🟦🟦",
58
- "Review the scholarly report and thematic map. "
59
- "**Submit Review** to finalise"),
60
- }
61
-
62
- PHASE_PROMPTS = {
63
- 0: ["Analyse my data set"],
64
- 1: ["Run analysis on abstracts", "Run analysis on titles",
65
- "Show data corpus statistics"],
66
- 2: ["Proceed to searching for themes", "Show initial codes",
67
- "How many orphan extracts?"],
68
- 3: ["Proceed to reviewing themes", "Show candidate themes",
69
- "Explain theme collation"],
70
- 4: ["Proceed to defining themes", "Show thematic map"],
71
- 5: ["Proceed to producing the report", "Show theme definitions",
72
- "Compare themes with PAJAIS taxonomy"],
73
- 6: ["Produce final scholarly report", "Show comparison table",
74
- "Export all results"],
75
- }
76
-
77
- REFERENCES_MD = """
78
- ## Methodology References
79
-
80
- Click any link to open the paper in a new tab. These are the foundational
81
- papers you can cite in your methodology section.
82
-
83
- ---
84
-
85
- ### 📖 Thematic Analysis (the method)
86
-
87
- **Braun, V., & Clarke, V. (2006).** Using thematic analysis in psychology.
88
- *Qualitative Research in Psychology*, 3(2), 77–101.
89
- 🔗 [DOI: 10.1191/1478088706qp063oa](https://doi.org/10.1191/1478088706qp063oa)
90
-
91
- > The foundational paper defining the six-phase reflexive thematic
92
- > analysis procedure. Cite this as the primary methodology reference.
93
- > Every phase name, terminology, and review step in this agent maps
94
- > directly to the procedures on pp. 87–93.
95
-
96
- **Braun, V., & Clarke, V. (2019).** Reflecting on reflexive thematic analysis.
97
- *Qualitative Research in Sport, Exercise and Health*, 11(4), 589–597.
98
- 🔗 [DOI: 10.1080/2159676X.2019.1628806](https://doi.org/10.1080/2159676X.2019.1628806)
99
-
100
- > A later clarification emphasising the reflexive, recursive, and
101
- > researcher-in-the-loop nature of the method. Useful for defending
102
- > the human-approval design of this agent.
103
-
104
- **Braun, V., & Clarke, V. (2021).** One size fits all? What counts as
105
- quality practice in (reflexive) thematic analysis? *Qualitative Research
106
- in Psychology*, 18(3), 328–352.
107
- 🔗 [DOI: 10.1080/14780887.2020.1769238](https://doi.org/10.1080/14780887.2020.1769238)
108
-
109
- > Quality criteria for thematic analysis — useful for defending the
110
- > STOP gate design as reviewer-approval checkpoints.
111
-
112
- ---
113
-
114
- ### 🧠 Embedding Model (Sentence-BERT)
115
-
116
- **Reimers, N., & Gurevych, I. (2019).** Sentence-BERT: Sentence Embeddings
117
- using Siamese BERT-Networks. *Proceedings of EMNLP-IJCNLP 2019*.
118
- 🔗 [arXiv: 1908.10084](https://arxiv.org/abs/1908.10084)
119
-
120
- > The paper behind `sentence-transformers/all-MiniLM-L6-v2`, the embedding
121
- > model used to convert data extracts into 384-dimensional vectors.
122
- > Establishes cosine similarity as the canonical comparison metric for
123
- > SBERT embeddings — justifies our use of cosine distance.
124
-
125
- ---
126
-
127
- ### 🔬 Topic Modelling Framework (BERTopic)
128
-
129
- **Grootendorst, M. (2022).** BERTopic: Neural topic modeling with a
130
- class-based TF-IDF procedure. *arXiv preprint*.
131
- 🔗 [arXiv: 2203.05794](https://arxiv.org/abs/2203.05794)
132
-
133
- > The BERTopic framework. Our approach follows its documented
134
- > Agglomerative Clustering configuration with `distance_threshold=0.5`
135
- > as a substitute for HDBSCAN when fine-grained control over code
136
- > granularity is required.
137
-
138
- ---
139
-
140
- ### ⚙️ Clustering Algorithm (scikit-learn)
141
-
142
- **Pedregosa, F., et al. (2011).** Scikit-learn: Machine Learning in Python.
143
- *Journal of Machine Learning Research*, 12, 2825–2830.
144
- 🔗 [JMLR](https://jmlr.org/papers/v12/pedregosa11a.html)
145
-
146
- > Cite this for `sklearn.cluster.AgglomerativeClustering` with
147
- > `metric='cosine'`, `linkage='average'`, `distance_threshold=0.50`.
148
-
149
- **Müllner, D. (2011).** Modern hierarchical, agglomerative clustering
150
- algorithms. *arXiv preprint*.
151
- 🔗 [arXiv: 1109.2378](https://arxiv.org/abs/1109.2378)
152
-
153
- > Comprehensive reference for agglomerative clustering algorithms and
154
- > linkage methods — useful for justifying the choice of `average`
155
- > linkage over `ward` for cosine-distance data.
156
-
157
- ---
158
-
159
- ### 🤖 Language Model (Mistral)
160
-
161
- **Jiang, A. Q., et al. (2023).** Mistral 7B. *arXiv preprint*.
162
- 🔗 [arXiv: 2310.06825](https://arxiv.org/abs/2310.06825)
163
-
164
- > The family of LLMs used for initial code labelling and narrative
165
- > generation. Our agent uses `mistral-large-latest` for these
166
- > LLM-dependent tool calls.
167
-
168
- ---
169
-
170
- ### 📚 LangChain / LangGraph
171
-
172
- **Chase, H., et al. (2023).** LangChain. *GitHub repository*.
173
- 🔗 [github.com/langchain-ai/langchain](https://github.com/langchain-ai/langchain)
174
-
175
- **Chase, H., et al. (2024).** LangGraph. *GitHub repository*.
176
- 🔗 [github.com/langchain-ai/langgraph](https://github.com/langchain-ai/langgraph)
177
-
178
- > The agent orchestration framework. `create_agent` (LangChain v1)
179
- > with `InMemorySaver` (LangGraph) provides the stateful multi-turn
180
- > conversation with tool-use capability underlying this agent.
181
-
182
- ---
183
-
184
- ### 🎨 User Interface (Gradio)
185
-
186
- **Abid, A., et al. (2019).** Gradio: Hassle-free sharing and testing of
187
- ML models in the wild. *arXiv preprint*.
188
- 🔗 [arXiv: 1906.02569](https://arxiv.org/abs/1906.02569)
189
-
190
- > The web UI framework. This application uses Gradio 6.x components:
191
- > `gr.Blocks`, `gr.Chatbot`, `gr.Dataframe`, `gr.File`, etc.
192
-
193
- ---
194
-
195
- ## How to cite this agent in your report
196
-
197
- > "Thematic analysis was conducted following Braun and Clarke's (2006)
198
- > six-phase reflexive procedure, computationally assisted using a
199
- > researcher-in-the-loop agent. Data extracts were embedded using
200
- > `all-MiniLM-L6-v2` (Reimers & Gurevych, 2019), clustered with
201
- > `sklearn.cluster.AgglomerativeClustering` (Pedregosa et al., 2011)
202
- > using `metric='cosine'`, `linkage='average'`, and
203
- > `distance_threshold=0.50`, following the Agglomerative Clustering
204
- > configuration documented in the BERTopic framework (Grootendorst, 2022).
205
- > Initial code labels and the final scholarly narrative were generated
206
- > using `mistral-large-latest` (Jiang et al., 2023). At every phase
207
- > boundary, the researcher reviewed and approved computational outputs
208
- > via a structured review table before the analysis advanced, preserving
209
- > the reflexive, recursive, and analyst-led character of thematic
210
- > analysis (Braun & Clarke, 2019; 2021)."
211
- """
212
-
213
-
214
- def _prompt_button_updates(phase: int) -> tuple:
215
- """Return gr.update values for the 4 phase-specific prompt buttons.
216
-
217
- Shows only prompts relevant to the current phase. Unused buttons
218
- are hidden (visible=False) so the UI stays clean.
219
-
220
- Returns:
221
- Tuple of 4 gr.update objects for btn1, btn2, btn3, btn4.
222
- """
223
- prompts = (PHASE_PROMPTS.get(phase, PHASE_PROMPTS[0]) + [""] * 4)[:4]
224
- return tuple(
225
- gr.update(value=p, visible=bool(p))
226
- for p in prompts
227
- )
228
-
229
- _path = lambda file: str(
230
- (hasattr(file, "name") and file.name)
231
- or (isinstance(file, str) and file)
232
- or ""
233
- )
234
- _name = lambda file: os.path.basename(_path(file))
235
-
236
-
237
- def _extract_phase(text: str) -> int:
238
- """Extract phase number from agent response. Returns 0 if not found."""
239
- found = re.findall(r"Phase (\d)", str(text))
240
- return int((found or ["0"])[0])
241
-
242
-
243
- def _phase_banner(num: int) -> str:
244
- """Generate prominent phase banner with progress bar and next step."""
245
- name, progress, instruction = PHASE_INFO.get(num, PHASE_INFO[0])
246
- return (
247
- f"## {progress} {name}\n\n"
248
- f"**NEXT STEP →** {instruction}"
249
- )
250
-
251
-
252
- def _load_review_table(base_dir: str) -> pd.DataFrame:
253
- """Load latest checkpoint file into the 9-column review table.
254
-
255
- Scans base_dir for topic_labels.json, themes.json, taxonomy_alignment.json,
256
- summaries.json. Loads the most recently modified one and formats it.
257
- Returns EMPTY_TABLE if nothing found.
258
- """
259
- base = Path(str(base_dir or "/tmp/nonexistent_dir_placeholder"))
260
- candidates = (
261
- base_dir and base.exists() and sorted(
262
- (
263
- list(base.glob("topic_labels.json"))
264
- + list(base.glob("themes.json"))
265
- + list(base.glob("taxonomy_alignment.json"))
266
- + list(base.glob("summaries.json"))
267
- ),
268
- key=lambda p: p.stat().st_mtime,
269
- reverse=True,
270
- )
271
- ) or []
272
-
273
- latest = (candidates[:1] or [None])[0]
274
- return (latest and [_format_checkpoint(latest)] or [EMPTY_TABLE.copy()])[0]
275
-
276
-
277
- def _format_checkpoint(path) -> pd.DataFrame:
278
- """Format a checkpoint JSON file into review table rows.
279
-
280
- Merges data from multiple checkpoint files when available:
281
- topic_labels.json has labels but no sizes — summaries.json has sizes.
282
- """
283
- raw = json.loads(Path(path).read_text())
284
- base = Path(path).parent
285
-
286
- data = (isinstance(raw, dict) and raw.get("clusters", raw.get("per_theme", []))) or \
287
- (isinstance(raw, list) and raw) or []
288
-
289
- summaries_data = {}
290
- summaries_path = base / "summaries.json"
291
- summaries_raw = (
292
- summaries_path.exists() and json.loads(summaries_path.read_text()) or {}
293
- )
294
- summaries_list = (
295
- isinstance(summaries_raw, dict) and summaries_raw.get("clusters", [])
296
- ) or (isinstance(summaries_raw, list) and summaries_raw) or []
297
- list(map(
298
- lambda s: summaries_data.update({s.get("topic_id", -999): s}),
299
- summaries_list,
300
- ))
301
-
302
- def _row(item: dict) -> dict:
303
- """Map one JSON item to review table columns, merging summaries data."""
304
- tid = item.get("topic_id", item.get("theme_id", 0))
305
- summary = summaries_data.get(tid, {})
306
- return {
307
- "#": tid,
308
- "Code / Theme Label": item.get("label", item.get("theme_label", "")),
309
- "Data Extract": str(
310
- item.get("representative", "")
311
- or summary.get("representative", "")
312
- or item.get("notes", "")
313
- )[:150],
314
- "Extracts": item.get("size", 0) or summary.get("size", 0)
315
- or item.get("total_papers", 0),
316
- "Data Items": item.get("size", 0) or summary.get("size", 0)
317
- or item.get("total_papers", 0),
318
- "Approve": "Yes",
319
- "Rename To": "",
320
- "Move To": "",
321
- "Analytic Memo": str(item.get("rationale",
322
- item.get("notes", ""))),
323
- }
324
-
325
- rows = list(map(_row, data[:200]))
326
- return (rows and [pd.DataFrame(rows, columns=REVIEW_COLS)] or [EMPTY_TABLE.copy()])[0]
327
-
328
-
329
- def on_file_upload(file):
330
- """Extract CSV stats and return updates for info, state, banner, buttons."""
331
- path = _path(file)
332
- default = (
333
- "Upload a CSV to begin.", "", _phase_banner(0),
334
- *_prompt_button_updates(0),
335
- )
336
- return (not path) and default or _do_file_upload(path, file)
337
-
338
-
339
- def _do_file_upload(path: str, file) -> tuple:
340
- """Actual file processing after path validation."""
341
- df = pd.read_csv(path)
342
- rows, cols = df.shape
343
- base = str(Path(path).parent)
344
- info = (
345
- f"**Loaded:** `{_name(file)}`\n\n"
346
- f"**Shape:** {rows:,} rows x {cols} columns\n\n"
347
- f"**Columns:** {', '.join(df.columns[:6].tolist())}\n\n"
348
- f"*Click a prompt below and press Send to begin.*"
349
- )
350
- return (info, base, _phase_banner(1), *_prompt_button_updates(1))
351
-
352
-
353
- def on_send(user_msg, history, file, base_dir):
354
- """Pass user message to agent. Update banner, table, and prompt buttons."""
355
- msg = (user_msg or "").strip() or "help"
356
- csv_tag = f"[CSV: {_path(file)}]\n" * bool(file)
357
-
358
- history = list(history or [])
359
- history.append({"role": "user", "content": msg})
360
- history.append({"role": "assistant", "content": "Thinking..."})
361
- yield (
362
- history, "", gr.skip(), gr.skip(), gr.skip(),
363
- gr.skip(), gr.skip(), gr.skip(), gr.skip(),
364
- )
365
-
366
- reply = agent_run(csv_tag + msg, thread_id=THREAD_ID)
367
- history[-1] = {"role": "assistant", "content": reply}
368
-
369
- phase = _extract_phase(reply)
370
- banner = _phase_banner(phase)
371
- table = _load_review_table(base_dir)
372
- btn_updates = _prompt_button_updates(phase)
373
-
374
- yield (history, "", banner, table, base_dir, *btn_updates)
375
-
376
-
377
- def on_submit_review(table_df, history, base_dir):
378
- """Serialise review table edits to agent. Return updated UI."""
379
- history = list(history or [])
380
- edits = table_df.to_json(orient="records", indent=2)
381
-
382
- history.append({"role": "user", "content": "[REVIEW SUBMITTED]"})
383
- history.append({"role": "assistant", "content": "Processing review..."})
384
-
385
- reply = agent_run(
386
- "Reviewer submitted table edits.\n\n"
387
- f"```json\n{edits}\n```\n\n"
388
- "Process: Approve/Reject decisions, Rename To values, "
389
- "Move To reassignments (call reassign_sentences if moves exist), "
390
- "Reasoning notes. Then check STOP gates and proceed.",
391
- thread_id=THREAD_ID,
392
- )
393
- history[-1] = {"role": "assistant", "content": reply}
394
-
395
- phase = _extract_phase(reply)
396
- return (
397
- history, _phase_banner(phase), _load_review_table(base_dir),
398
- *_prompt_button_updates(phase),
399
- )
400
-
401
-
402
- def on_download(table_df, history):
403
- """Export review CSV and chat TXT."""
404
- csv_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="review_")
405
- table_df.to_csv(csv_tmp.name, index=False)
406
-
407
- txt_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", prefix="chat_")
408
- txt_tmp.write(
409
- "\n\n".join(
410
- list(map(
411
- lambda m: f"{m.get('role', '').upper()}: {m.get('content', '')}",
412
- history or [],
413
- ))
414
- ).encode("utf-8")
415
- )
416
- txt_tmp.close()
417
- return [csv_tmp.name, txt_tmp.name]
418
-
419
-
420
- with gr.Blocks(title="Thematic Analysis Agent") as demo:
421
-
422
- base_dir_state = gr.State(value="")
423
-
424
- gr.Markdown("# Thematic Analysis Agent")
425
- gr.Markdown(
426
- "**Braun & Clarke (2006) 6-Phase Reflexive Thematic Analysis** "
427
- "| Sentence-BERT Embeddings | Agglomerative Clustering | "
428
- "Cosine Distance 0.50"
429
- )
430
-
431
- phase_banner = gr.Markdown(value=_phase_banner(0))
432
-
433
- with gr.Tabs():
434
-
435
- with gr.Tab("🔬 Analysis"):
436
- gr.Markdown("---\n### Section 1 — Data Corpus")
437
- with gr.Row():
438
- with gr.Column(scale=3):
439
- file_input = gr.File(
440
- label="Upload data corpus (Scopus CSV)",
441
- file_types=[".csv"],
442
- file_count="single",
443
- )
444
- with gr.Column(scale=5):
445
- file_info = gr.Markdown("Upload a CSV to begin.")
446
-
447
- gr.Markdown("---\n### Section 2 — Analyst Dialogue")
448
- chatbot = gr.Chatbot(label="Thematic Analysis Agent", height=200)
449
- with gr.Row():
450
- msg_box = gr.Textbox(
451
- placeholder="Type a message or click a phase action below",
452
- show_label=False, scale=7, lines=1,
453
- )
454
- send_btn = gr.Button("Send", variant="primary", scale=1)
455
-
456
- gr.Markdown("**Phase actions** (click to proceed — only actions "
457
- "valid for the current B&C phase are shown)")
458
- with gr.Row():
459
- prompt_btn_1 = gr.Button("Analyse my data set",
460
- variant="secondary", scale=1, size="sm")
461
- prompt_btn_2 = gr.Button("", variant="secondary", scale=1,
462
- size="sm", visible=False)
463
- prompt_btn_3 = gr.Button("", variant="secondary", scale=1,
464
- size="sm", visible=False)
465
- prompt_btn_4 = gr.Button("", variant="secondary", scale=1,
466
- size="sm", visible=False)
467
-
468
- gr.Markdown("---\n### Section 3 — Initial Codes / Candidate Themes / Themes")
469
- gr.Markdown(
470
- "Auto-populated from tool outputs. Labels are **initial codes** "
471
- "in Phase 2, **candidate themes** in Phase 3, and **themes** in "
472
- "Phases 4–6. Edit **Approve**, **Rename To**, **Move To**, "
473
- "**Analytic Memo** columns, then click **Submit Review**."
474
- )
475
- review_table = gr.Dataframe(
476
- value=EMPTY_TABLE,
477
- headers=REVIEW_COLS,
478
- datatype=["number", "str", "str", "number", "number",
479
- "str", "str", "str", "str"],
480
- column_count=(9, "fixed"),
481
- interactive=True,
482
- wrap=True,
483
- max_height=400,
484
- )
485
- with gr.Row():
486
- clear_btn = gr.Button("Clear table", variant="secondary", scale=2)
487
- sub_btn = gr.Button("Submit Review", variant="primary", scale=4)
488
-
489
- with gr.Accordion("Download", open=False):
490
- dl_btn = gr.Button("Generate downloads", variant="primary")
491
- dl_files = gr.File(label="Downloads", file_count="multiple",
492
- interactive=False)
493
-
494
- with gr.Tab("📚 References"):
495
- gr.Markdown(REFERENCES_MD)
496
-
497
- file_input.change(
498
- on_file_upload,
499
- inputs=[file_input],
500
- outputs=[file_info, base_dir_state, phase_banner,
501
- prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
502
- )
503
- send_btn.click(
504
- on_send,
505
- inputs=[msg_box, chatbot, file_input, base_dir_state],
506
- outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
507
- prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
508
- )
509
- msg_box.submit(
510
- on_send,
511
- inputs=[msg_box, chatbot, file_input, base_dir_state],
512
- outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
513
- prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
514
- )
515
- prompt_btn_1.click(
516
- on_send,
517
- inputs=[prompt_btn_1, chatbot, file_input, base_dir_state],
518
- outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
519
- prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
520
- )
521
- prompt_btn_2.click(
522
- on_send,
523
- inputs=[prompt_btn_2, chatbot, file_input, base_dir_state],
524
- outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
525
- prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
526
- )
527
- prompt_btn_3.click(
528
- on_send,
529
- inputs=[prompt_btn_3, chatbot, file_input, base_dir_state],
530
- outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
531
- prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
532
- )
533
- prompt_btn_4.click(
534
- on_send,
535
- inputs=[prompt_btn_4, chatbot, file_input, base_dir_state],
536
- outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
537
- prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
538
- )
539
- clear_btn.click(lambda: EMPTY_TABLE.copy(), outputs=[review_table])
540
- sub_btn.click(
541
- on_submit_review,
542
- inputs=[review_table, chatbot, base_dir_state],
543
- outputs=[chatbot, phase_banner, review_table,
544
- prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
545
- )
546
- dl_btn.click(on_download, inputs=[review_table, chatbot], outputs=[dl_files])
547
-
548
- demo.launch(ssr_mode=False, theme=gr.themes.Soft())