milindkamat0507 commited on
Commit
304331d
Β·
verified Β·
1 Parent(s): 81d9f86

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +581 -0
app.py ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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:
281
+ topic_labels.json has labels/rationale (from Mistral LLM)
282
+ summaries.json has sizes/indices/representative text (deterministic)
283
+ Normalises topic_id to int for cross-file joins. Tries multiple key
284
+ name variants for robustness against LLM output variation.
285
+ """
286
+ raw = json.loads(Path(path).read_text())
287
+ base = Path(path).parent
288
+
289
+ data = (isinstance(raw, dict) and raw.get("clusters", raw.get(
290
+ "per_theme", raw.get("topics", raw.get("themes", []))))) or \
291
+ (isinstance(raw, list) and raw) or []
292
+
293
+ _get_tid = lambda d: int(d.get("topic_id",
294
+ d.get("theme_id",
295
+ d.get("id",
296
+ d.get("cluster_id", -1)))))
297
+
298
+ summaries_data = {}
299
+ summaries_path = base / "summaries.json"
300
+ summaries_raw = (
301
+ summaries_path.exists() and json.loads(summaries_path.read_text()) or {}
302
+ )
303
+ summaries_list = (
304
+ isinstance(summaries_raw, dict) and summaries_raw.get("clusters", [])
305
+ ) or (isinstance(summaries_raw, list) and summaries_raw) or []
306
+ list(map(
307
+ lambda s: summaries_data.update({_get_tid(s): s}),
308
+ summaries_list,
309
+ ))
310
+
311
+ labels_data = {}
312
+ labels_path = base / "topic_labels.json"
313
+ labels_raw = (
314
+ labels_path.exists() and path.name != "topic_labels.json"
315
+ and json.loads(labels_path.read_text()) or {}
316
+ )
317
+ labels_list = (
318
+ isinstance(labels_raw, dict) and labels_raw.get("clusters",
319
+ labels_raw.get("topics", labels_raw.get("themes", [])))
320
+ ) or (isinstance(labels_raw, list) and labels_raw) or []
321
+ list(map(
322
+ lambda l: labels_data.update({_get_tid(l): l}),
323
+ labels_list,
324
+ ))
325
+
326
+ _label_of = lambda d: (
327
+ d.get("label") or d.get("Label") or d.get("name") or
328
+ d.get("topic_label") or d.get("theme_label") or
329
+ d.get("title") or ""
330
+ )
331
+
332
+ def _row(item: dict) -> dict:
333
+ """Map one JSON item to review table columns, merging all sources."""
334
+ tid = _get_tid(item)
335
+ summary = summaries_data.get(tid, {})
336
+ labels = labels_data.get(tid, {})
337
+ label = _label_of(item) or _label_of(labels) or f"code_{tid}"
338
+ extract = (
339
+ item.get("representative") or summary.get("representative")
340
+ or labels.get("representative") or item.get("notes", "")
341
+ )
342
+ size = (item.get("size") or summary.get("size")
343
+ or item.get("total_papers") or 0)
344
+ memo = (item.get("rationale") or labels.get("rationale")
345
+ or item.get("notes") or "")
346
+ return {
347
+ "#": tid,
348
+ "Code / Theme Label": str(label),
349
+ "Data Extract": str(extract)[:150],
350
+ "Extracts": size,
351
+ "Data Items": size,
352
+ "Approve": "Yes",
353
+ "Rename To": "",
354
+ "Move To": "",
355
+ "Analytic Memo": str(memo),
356
+ }
357
+
358
+ rows = list(map(_row, data[:200]))
359
+ return (rows and [pd.DataFrame(rows, columns=REVIEW_COLS)] or [EMPTY_TABLE.copy()])[0]
360
+
361
+
362
+ def on_file_upload(file):
363
+ """Extract CSV stats and return updates for info, state, banner, buttons."""
364
+ path = _path(file)
365
+ default = (
366
+ "Upload a CSV to begin.", "", _phase_banner(0),
367
+ *_prompt_button_updates(0),
368
+ )
369
+ return (not path) and default or _do_file_upload(path, file)
370
+
371
+
372
+ def _do_file_upload(path: str, file) -> tuple:
373
+ """Actual file processing after path validation."""
374
+ df = pd.read_csv(path)
375
+ rows, cols = df.shape
376
+ base = str(Path(path).parent)
377
+ info = (
378
+ f"**Loaded:** `{_name(file)}`\n\n"
379
+ f"**Shape:** {rows:,} rows x {cols} columns\n\n"
380
+ f"**Columns:** {', '.join(df.columns[:6].tolist())}\n\n"
381
+ f"*Click a prompt below and press Send to begin.*"
382
+ )
383
+ return (info, base, _phase_banner(1), *_prompt_button_updates(1))
384
+
385
+
386
+ def on_send(user_msg, history, file, base_dir):
387
+ """Pass user message to agent. Update banner, table, and prompt buttons."""
388
+ msg = (user_msg or "").strip() or "help"
389
+ csv_tag = f"[CSV: {_path(file)}]\n" * bool(file)
390
+
391
+ history = list(history or [])
392
+ history.append({"role": "user", "content": msg})
393
+ history.append({"role": "assistant", "content": "Thinking..."})
394
+ yield (
395
+ history, "", gr.skip(), gr.skip(), gr.skip(),
396
+ gr.skip(), gr.skip(), gr.skip(), gr.skip(),
397
+ )
398
+
399
+ reply = agent_run(csv_tag + msg, thread_id=THREAD_ID)
400
+ history[-1] = {"role": "assistant", "content": reply}
401
+
402
+ phase = _extract_phase(reply)
403
+ banner = _phase_banner(phase)
404
+ table = _load_review_table(base_dir)
405
+ btn_updates = _prompt_button_updates(phase)
406
+
407
+ yield (history, "", banner, table, base_dir, *btn_updates)
408
+
409
+
410
+ def on_submit_review(table_df, history, base_dir):
411
+ """Serialise review table edits to agent. Return updated UI."""
412
+ history = list(history or [])
413
+ edits = table_df.to_json(orient="records", indent=2)
414
+
415
+ history.append({"role": "user", "content": "[REVIEW SUBMITTED]"})
416
+ history.append({"role": "assistant", "content": "Processing review..."})
417
+
418
+ reply = agent_run(
419
+ "Reviewer submitted table edits.\n\n"
420
+ f"```json\n{edits}\n```\n\n"
421
+ "Process: Approve/Reject decisions, Rename To values, "
422
+ "Move To reassignments (call reassign_sentences if moves exist), "
423
+ "Reasoning notes. Then check STOP gates and proceed.",
424
+ thread_id=THREAD_ID,
425
+ )
426
+ history[-1] = {"role": "assistant", "content": reply}
427
+
428
+ phase = _extract_phase(reply)
429
+ return (
430
+ history, _phase_banner(phase), _load_review_table(base_dir),
431
+ *_prompt_button_updates(phase),
432
+ )
433
+
434
+
435
+ def on_download(table_df, history):
436
+ """Export review CSV and chat TXT."""
437
+ csv_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", prefix="review_")
438
+ table_df.to_csv(csv_tmp.name, index=False)
439
+
440
+ txt_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".txt", prefix="chat_")
441
+ txt_tmp.write(
442
+ "\n\n".join(
443
+ list(map(
444
+ lambda m: f"{m.get('role', '').upper()}: {m.get('content', '')}",
445
+ history or [],
446
+ ))
447
+ ).encode("utf-8")
448
+ )
449
+ txt_tmp.close()
450
+ return [csv_tmp.name, txt_tmp.name]
451
+
452
+
453
+ with gr.Blocks(title="Thematic Analysis Agent") as demo:
454
+
455
+ base_dir_state = gr.State(value="")
456
+
457
+ gr.Markdown("# Thematic Analysis Agent")
458
+ gr.Markdown(
459
+ "**Braun & Clarke (2006) 6-Phase Reflexive Thematic Analysis** "
460
+ "| Sentence-BERT Embeddings | Agglomerative Clustering | "
461
+ "Cosine Distance 0.50"
462
+ )
463
+
464
+ phase_banner = gr.Markdown(value=_phase_banner(0))
465
+
466
+ with gr.Tabs():
467
+
468
+ with gr.Tab("πŸ”¬ Analysis"):
469
+ gr.Markdown("---\n### Section 1 β€” Data Corpus")
470
+ with gr.Row():
471
+ with gr.Column(scale=3):
472
+ file_input = gr.File(
473
+ label="Upload data corpus (Scopus CSV)",
474
+ file_types=[".csv"],
475
+ file_count="single",
476
+ )
477
+ with gr.Column(scale=5):
478
+ file_info = gr.Markdown("Upload a CSV to begin.")
479
+
480
+ gr.Markdown("---\n### Section 2 β€” Analyst Dialogue")
481
+ chatbot = gr.Chatbot(label="Thematic Analysis Agent", height=200)
482
+ with gr.Row():
483
+ msg_box = gr.Textbox(
484
+ placeholder="Type a message or click a phase action below",
485
+ show_label=False, scale=7, lines=1,
486
+ )
487
+ send_btn = gr.Button("Send", variant="primary", scale=1)
488
+
489
+ gr.Markdown("**Phase actions** (click to proceed β€” only actions "
490
+ "valid for the current B&C phase are shown)")
491
+ with gr.Row():
492
+ prompt_btn_1 = gr.Button("Analyse my data set",
493
+ variant="secondary", scale=1, size="sm")
494
+ prompt_btn_2 = gr.Button("", variant="secondary", scale=1,
495
+ size="sm", visible=False)
496
+ prompt_btn_3 = gr.Button("", variant="secondary", scale=1,
497
+ size="sm", visible=False)
498
+ prompt_btn_4 = gr.Button("", variant="secondary", scale=1,
499
+ size="sm", visible=False)
500
+
501
+ gr.Markdown("---\n### Section 3 β€” Initial Codes / Candidate Themes / Themes")
502
+ gr.Markdown(
503
+ "Auto-populated from tool outputs. Labels are **initial codes** "
504
+ "in Phase 2, **candidate themes** in Phase 3, and **themes** in "
505
+ "Phases 4–6. Edit **Approve**, **Rename To**, **Move To**, "
506
+ "**Analytic Memo** columns, then click **Submit Review**."
507
+ )
508
+ review_table = gr.Dataframe(
509
+ value=EMPTY_TABLE,
510
+ headers=REVIEW_COLS,
511
+ datatype=["number", "str", "str", "number", "number",
512
+ "str", "str", "str", "str"],
513
+ column_count=(9, "fixed"),
514
+ interactive=True,
515
+ wrap=True,
516
+ max_height=400,
517
+ )
518
+ with gr.Row():
519
+ clear_btn = gr.Button("Clear table", variant="secondary", scale=2)
520
+ sub_btn = gr.Button("Submit Review", variant="primary", scale=4)
521
+
522
+ with gr.Accordion("Download", open=False):
523
+ dl_btn = gr.Button("Generate downloads", variant="primary")
524
+ dl_files = gr.File(label="Downloads", file_count="multiple",
525
+ interactive=False)
526
+
527
+ with gr.Tab("πŸ“š References"):
528
+ gr.Markdown(REFERENCES_MD)
529
+
530
+ file_input.change(
531
+ on_file_upload,
532
+ inputs=[file_input],
533
+ outputs=[file_info, base_dir_state, phase_banner,
534
+ prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
535
+ )
536
+ send_btn.click(
537
+ on_send,
538
+ inputs=[msg_box, chatbot, file_input, base_dir_state],
539
+ outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
540
+ prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
541
+ )
542
+ msg_box.submit(
543
+ on_send,
544
+ inputs=[msg_box, chatbot, file_input, base_dir_state],
545
+ outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
546
+ prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
547
+ )
548
+ prompt_btn_1.click(
549
+ on_send,
550
+ inputs=[prompt_btn_1, chatbot, file_input, base_dir_state],
551
+ outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
552
+ prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
553
+ )
554
+ prompt_btn_2.click(
555
+ on_send,
556
+ inputs=[prompt_btn_2, chatbot, file_input, base_dir_state],
557
+ outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
558
+ prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
559
+ )
560
+ prompt_btn_3.click(
561
+ on_send,
562
+ inputs=[prompt_btn_3, chatbot, file_input, base_dir_state],
563
+ outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
564
+ prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
565
+ )
566
+ prompt_btn_4.click(
567
+ on_send,
568
+ inputs=[prompt_btn_4, chatbot, file_input, base_dir_state],
569
+ outputs=[chatbot, msg_box, phase_banner, review_table, base_dir_state,
570
+ prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
571
+ )
572
+ clear_btn.click(lambda: EMPTY_TABLE.copy(), outputs=[review_table])
573
+ sub_btn.click(
574
+ on_submit_review,
575
+ inputs=[review_table, chatbot, base_dir_state],
576
+ outputs=[chatbot, phase_banner, review_table,
577
+ prompt_btn_1, prompt_btn_2, prompt_btn_3, prompt_btn_4],
578
+ )
579
+ dl_btn.click(on_download, inputs=[review_table, chatbot], outputs=[dl_files])
580
+
581
+ demo.launch(ssr_mode=False, theme=gr.themes.Soft())