amirali1985 commited on
Commit
76810fa
ยท
1 Parent(s): 123f01a

divergence only.

Browse files
Files changed (2) hide show
  1. app.py +134 -250
  2. backup_app.py +339 -0
app.py CHANGED
@@ -1,36 +1,30 @@
1
 
2
- # sjt_answers_viewer.py
3
- # Gradio viewer for case_study_answers.json
4
- # - Shows one question at a time
5
- # - Dropdown 1: filter by **Name**
6
- # - Dropdown 2: filter by **Selected Trait** (HEXACO slice)
7
- # - Underlines & highlights the actually selected option + trait name
8
- # - Always orders options consistently (HEXACO)
9
- # - Top "Summary" bar shows proportion of selections by trait (under current Name filter)
10
 
11
  import json
12
  from pathlib import Path
13
  from typing import List, Dict, Any, Optional, Tuple
14
  import random
15
-
16
  import gradio as gr
17
- import matplotlib.pyplot as plt
18
-
19
- # ---------- Constants ----------
20
 
21
  DATA_PATH = Path("case_study_answers.json")
22
 
23
- HEXACO_ORDER = [
24
- "hh",
 
25
  "emotionality",
26
  "extraversion",
27
  "agreeableness",
28
  "conscientiousness",
29
  "openness",
30
  ]
31
-
32
  TRAIT_LABELS = {
33
- "hh": "Honestyโ€“Humility",
34
  "emotionality": "Emotionality",
35
  "extraversion": "Extraversion",
36
  "agreeableness": "Agreeableness",
@@ -38,19 +32,35 @@ TRAIT_LABELS = {
38
  "openness": "Openness",
39
  }
40
 
41
- # ---------- Icons ----------
42
-
43
- ICONS = {
44
- "header": "๐Ÿ“",
45
- "question": "โ“",
46
- "options": "โœ…",
47
- "summary": "๐Ÿ“Š",
48
- "progress": "โญ๏ธ",
49
- "metadata": "๐Ÿ”–",
50
- "filters": "๐ŸŽ›๏ธ",
 
51
  }
52
 
53
- # ---------- Data Loading & Normalization ----------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  def load_json(path: Path) -> Any:
56
  if not path.exists():
@@ -59,65 +69,40 @@ def load_json(path: Path) -> Any:
59
  return json.load(f)
60
 
61
  def _safe_get_question_block(item: Dict[str, Any]) -> Tuple[str, Dict[str, str], Optional[str]]:
62
- """
63
- Extract (question_text, options_map, selected_trait) from a raw item.
64
- Heuristics:
65
- - Selected trait is at top-level key 'option'.
66
- - Question text/options may be under item['question'] with nested 'corrected_sjt' or 'original_sjt'.
67
- - Options are expected as keys like '<trait>_option' where trait โˆˆ HEXACO_ORDER.
68
- """
69
- selected = item.get("option")
70
-
71
  q = item.get("question", {}) or {}
72
  block = q.get("corrected_sjt") or q.get("original_sjt") or {}
73
 
74
  question_text = ""
75
  options: Dict[str, str] = {}
76
-
77
  if isinstance(block, dict):
78
  question_text = block.get("question") or q.get("question") or ""
79
- for trait in HEXACO_ORDER:
80
- k = f"{trait}_option"
81
- if k in block:
82
- options[trait] = str(block[k]).strip()
83
- elif k in q:
84
- options[trait] = str(q[k]).strip()
85
  else:
86
- # sometimes block is a plain string
87
  question_text = str(block) if block else str(q.get("question", ""))
88
 
89
- # Fallback: look for options directly on item if missing
90
  if not options and isinstance(q, dict):
91
- for trait in HEXACO_ORDER:
92
- k = f"{trait}_option"
93
- if k in q:
94
- options[trait] = str(q[k]).strip()
95
-
96
  return question_text.strip(), options, selected
97
 
98
  def flatten_entries(raw: Any) -> List[Dict[str, Any]]:
99
- """
100
- Returns a list of entries with keys:
101
- - name (str)
102
- - question (str)
103
- - options (dict[trait->text])
104
- - selected (trait str)
105
- """
106
  out: List[Dict[str, Any]] = []
107
-
108
  def handle_item(obj: Dict[str, Any], default_name: str):
109
  q_text, opts, sel = _safe_get_question_block(obj)
110
- # Prefer name from object if present; else inherit from container
111
  nm = (obj.get("name") or default_name or "Unknown").strip() or "Unknown"
112
  if q_text and opts and sel:
113
  out.append({"name": nm, "question": q_text, "options": opts, "selected": sel})
114
-
115
  if isinstance(raw, list):
116
  for x in raw:
117
  if isinstance(x, dict):
118
  handle_item(x, "Unknown")
119
  elif isinstance(raw, dict):
120
- # Could be {persona_name: [items]} or {persona_name: {...}} etc.
121
  for k, v in raw.items():
122
  default_name = str(k)
123
  if isinstance(v, list):
@@ -128,140 +113,70 @@ def flatten_entries(raw: Any) -> List[Dict[str, Any]]:
128
  handle_item(v, default_name)
129
  return out
130
 
131
- DATA_RAW = load_json(DATA_PATH)
132
- DATA: List[Dict[str, Any]] = flatten_entries(DATA_RAW)
133
-
134
- # Unique names for dropdown
135
- def all_names(entries: List[Dict[str, Any]]) -> List[str]:
136
- seen = []
137
- for e in entries:
138
- nm = e.get("name", "Unknown") or "Unknown"
139
- if nm not in seen:
140
- seen.append(nm)
141
- return sorted(seen)
142
-
143
- NAME_FILTERS = ["All"] + all_names(DATA)
144
- TRAIT_FILTERS = ["All"] + HEXACO_ORDER
145
-
146
- # ---------- Filtering & Navigation ----------
147
-
148
- def get_filtered_indices(entries: List[Dict[str, Any]], name_filt: str, trait_filt: str) -> List[int]:
149
- idxs = list(range(len(entries)))
150
- if name_filt != "All":
151
- idxs = [i for i in idxs if entries[i].get("name") == name_filt]
152
- if trait_filt != "All":
153
- idxs = [i for i in idxs if entries[i].get("selected") == trait_filt]
154
- return idxs
155
-
156
- def clamp_index(i: int, n: int) -> int:
157
- return 0 if n == 0 else (i % n)
158
-
159
- # ---------- Summary ----------
160
 
161
- def compute_summary(entries: List[Dict[str, Any]]):
162
- total = len(entries)
163
- counts = {t: 0 for t in HEXACO_ORDER}
 
 
164
  for e in entries:
165
- sel = e.get("selected")
166
- if sel in counts:
167
- counts[sel] += 1
168
- labels = [TRAIT_LABELS[t] for t in HEXACO_ORDER]
169
- props = [counts[t] / total if total else 0.0 for t in HEXACO_ORDER]
170
- return labels, props, counts, total
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
- def summary_plot(entries: List[Dict[str, Any]]):
173
- # Returns Markdown with proportions per trait under the current Name filter
174
- labels, props, counts, total = compute_summary(entries)
175
- lines = ["## ๐Ÿ“Š Summary (Name filter)", f"**Total:** {total}"]
176
- for label, p in zip(labels, props):
177
- lines.append(f"- {label}: {p:.2f}")
178
- return "\n".join(lines)
179
-
180
- # ---------- Rendering ----------
181
-
182
- def md_question(entry: Dict[str, Any]) -> str:
183
- q = entry.get("question", "")
184
- name = entry.get("name", "โ€”")
185
- return f"## {ICONS['question']} Question\n**Name:** {name}\n\n{q if q else 'โ€”'}"
186
-
187
- def md_options(entry: Dict[str, Any]) -> str:
188
- opts: Dict[str, str] = entry.get("options", {})
189
- selected = entry.get("selected")
190
- lines = []
191
- for i, trait in enumerate(HEXACO_ORDER, start=1):
192
- if trait not in opts:
193
- continue
194
- label = TRAIT_LABELS[trait]
195
- text = opts[trait]
196
- if trait == selected:
197
- # underline + highlight both the label and the text
198
- line = (
199
- f"{i}. <u><mark><strong>{label}</strong>:</mark></u> "
200
- f"<u><mark>{text}</mark></u>"
201
- )
202
- else:
203
- line = f"{i}. **{label}:** {text}"
204
- lines.append(line)
205
- body = "\n\n".join(lines) if lines else "โ€”"
206
- return f"## {ICONS['options']} Options (HEXACO order)\n{body}"
207
-
208
- def md_metadata(entry: Dict[str, Any], idx: int, total_in_filter: int) -> str:
209
- sel = entry.get("selected", "โ€”")
210
- sel_disp = TRAIT_LABELS.get(sel, sel)
211
- nm = entry.get("name", "โ€”")
212
- return (
213
- f"## {ICONS['metadata']} Metadata\n"
214
- f"**Name:** {nm} \n"
215
- f"**Selected Option (Trait):** {sel_disp} \n"
216
- f"**Position in Filter:** {idx + 1} / {total_in_filter}"
217
- )
218
-
219
- def md_progress(idx: int, total: int) -> str:
220
- return f"## {ICONS['progress']} Progress\n**{idx + 1} / {total}**"
221
-
222
- def render(entries: List[Dict[str, Any]], name_filt: str, trait_filt: str, pos: int):
223
- # For summary, use "name-only" filter to show that persona's distribution
224
- name_only_indices = [i for i, e in enumerate(entries) if (name_filt == "All" or e.get("name") == name_filt)]
225
- name_only_slice = [entries[i] for i in name_only_indices]
226
-
227
- # For the main view selection, apply both filters
228
- indices = get_filtered_indices(entries, name_filt, trait_filt)
229
- n = len(indices)
230
-
231
- if n == 0:
232
- return (
233
- summary_plot(name_only_slice),
234
- f"## {ICONS['question']} Question\n_No questions for filters **Name={name_filt}**, **Trait={trait_filt}**._",
235
- f"## {ICONS['options']} Options\nโ€”",
236
- f"## {ICONS['metadata']} Metadata\nโ€”",
237
- f"## {ICONS['progress']} Progress\n0 / 0",
238
- 0, # expose pos back
239
- )
240
-
241
- pos = clamp_index(pos, n)
242
- entry = entries[indices[pos]]
243
- return (
244
- summary_plot(name_only_slice),
245
- md_question(entry),
246
- md_options(entry),
247
- md_metadata(entry, pos, n),
248
- md_progress(pos, n),
249
- pos, # expose pos back
250
- )
251
-
252
- # ---------- Gradio App ----------
253
 
254
- with gr.Blocks(title="SJT Answers Viewer") as demo:
255
- gr.Markdown("# SJT Answers Viewer")
256
- gr.Markdown(
257
- f"{ICONS['filters']} **Filters:** Choose a Name and a HEXACO Selected-Trait slice.\n\n"
258
- f"{ICONS['summary']} **Summary:** Bar shows the trait-selection proportions under the current **Name** filter.\n\n"
259
- "Options are consistently ordered by HEXACO. The actual selected option is underlined and highlighted."
260
- )
261
 
262
  with gr.Row():
263
- name_dd = gr.Dropdown(choices=NAME_FILTERS, value="All", label="Filter by Name", interactive=True)
264
- trait_dd = gr.Dropdown(choices=TRAIT_FILTERS, value="All", label="Filter by Selected Trait", interactive=True)
265
  st_pos = gr.State(0)
266
 
267
  with gr.Row():
@@ -269,71 +184,40 @@ with gr.Blocks(title="SJT Answers Viewer") as demo:
269
  next_btn = gr.Button("Next")
270
  rand_btn = gr.Button("Random")
271
 
272
- # Outputs
273
- summary_out = gr.Markdown(label="Selections Summary (Name filter)")
274
- question_out = gr.Markdown()
275
- options_out = gr.Markdown()
276
- metadata_out = gr.Markdown()
277
- progress_out = gr.Markdown()
278
-
279
- # ----- Callbacks -----
280
- def on_filters_change(name_filt: str, trait_filt: str):
281
- return [*render(DATA, name_filt, trait_filt, 0), 0]
282
-
283
- def on_prev(name_filt: str, trait_filt: str, pos: int):
284
- indices = get_filtered_indices(DATA, name_filt, trait_filt)
285
- if not indices:
286
- return [*render(DATA, name_filt, trait_filt, pos), pos]
287
- pos = clamp_index(pos - 1, len(indices))
288
- return [*render(DATA, name_filt, trait_filt, pos), pos]
289
-
290
- def on_next(name_filt: str, trait_filt: str, pos: int):
291
- indices = get_filtered_indices(DATA, name_filt, trait_filt)
292
- if not indices:
293
- return [*render(DATA, name_filt, trait_filt, pos), pos]
294
- pos = clamp_index(pos + 1, len(indices))
295
- return [*render(DATA, name_filt, trait_filt, pos), pos]
296
-
297
- def on_rand(name_filt: str, trait_filt: str, pos: int):
298
- indices = get_filtered_indices(DATA, name_filt, trait_filt)
299
- if not indices:
300
- return [*render(DATA, name_filt, trait_filt, pos), pos]
301
- pos = random.randrange(len(indices))
302
- return [*render(DATA, name_filt, trait_filt, pos), pos]
303
 
304
- name_dd.change(
305
- on_filters_change,
306
- inputs=[name_dd, trait_dd],
307
- outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
308
- )
309
- trait_dd.change(
310
- on_filters_change,
311
- inputs=[name_dd, trait_dd],
312
- outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
313
- )
314
 
315
- prev_btn.click(
316
- on_prev,
317
- inputs=[name_dd, trait_dd, st_pos],
318
- outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
319
- )
320
- next_btn.click(
321
- on_next,
322
- inputs=[name_dd, trait_dd, st_pos],
323
- outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
324
- )
325
- rand_btn.click(
326
- on_rand,
327
- inputs=[name_dd, trait_dd, st_pos],
328
- outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
329
- )
330
 
331
  # initial load
332
- demo.load(
333
- lambda: [*render(DATA, "All", "All", 0), 0],
334
- inputs=None,
335
- outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
336
- )
337
 
338
  if __name__ == "__main__":
339
  demo.launch()
 
1
 
2
+ # sjt_compare_eleanor_hung.py
3
+ # Minimal Gradio app: show ONLY questions where Eleanor and Hung chose different options.
4
+ # - Loads case_study_answers.json
5
+ # - Compares two personas (default: "Eleanor" vs "Hung")
6
+ # - Displays the question and highlights Eleanor's choice in green, Hung's in red
7
+ # - Navigation: Previous / Next / Random
 
 
8
 
9
  import json
10
  from pathlib import Path
11
  from typing import List, Dict, Any, Optional, Tuple
12
  import random
 
13
  import gradio as gr
 
 
 
14
 
15
  DATA_PATH = Path("case_study_answers.json")
16
 
17
+ # Canonical HEXACO order & labels
18
+ CANONICAL_ORDER = [
19
+ "honesty_humility",
20
  "emotionality",
21
  "extraversion",
22
  "agreeableness",
23
  "conscientiousness",
24
  "openness",
25
  ]
 
26
  TRAIT_LABELS = {
27
+ "honesty_humility": "Honestyโ€“Humility",
28
  "emotionality": "Emotionality",
29
  "extraversion": "Extraversion",
30
  "agreeableness": "Agreeableness",
 
32
  "openness": "Openness",
33
  }
34
 
35
+ ALIAS_TO_CANON = {
36
+ "hh": "honesty_humility",
37
+ "honesty_humility": "honesty_humility",
38
+ "honesty-humility": "honesty_humility",
39
+ "honestyhumility": "honesty_humility",
40
+ "honesty": "honesty_humility",
41
+ "emotionality": "emotionality",
42
+ "extraversion": "extraversion",
43
+ "agreeableness": "agreeableness",
44
+ "conscientiousness": "conscientiousness",
45
+ "openness": "openness",
46
  }
47
 
48
+ def canonical_trait(x: Optional[str]) -> Optional[str]:
49
+ if x is None:
50
+ return None
51
+ s = str(x).strip().lower()
52
+ if s.endswith("_option"):
53
+ s = s[:-7]
54
+ s = s.replace("-", "_").replace(" ", "_")
55
+ return ALIAS_TO_CANON.get(s, s if s in CANONICAL_ORDER else None)
56
+
57
+ def get_option_text_from_blocks(block: Dict[str, Any], q: Dict[str, Any], canon: str) -> Optional[str]:
58
+ for key in (f"{canon}_option", f"hh_option" if canon == "honesty_humility" else None):
59
+ if key and isinstance(block, dict) and key in block:
60
+ return str(block[key]).strip()
61
+ if key and isinstance(q, dict) and key in q:
62
+ return str(q[key]).strip()
63
+ return None
64
 
65
  def load_json(path: Path) -> Any:
66
  if not path.exists():
 
69
  return json.load(f)
70
 
71
  def _safe_get_question_block(item: Dict[str, Any]) -> Tuple[str, Dict[str, str], Optional[str]]:
72
+ selected = canonical_trait(item.get("option"))
 
 
 
 
 
 
 
 
73
  q = item.get("question", {}) or {}
74
  block = q.get("corrected_sjt") or q.get("original_sjt") or {}
75
 
76
  question_text = ""
77
  options: Dict[str, str] = {}
 
78
  if isinstance(block, dict):
79
  question_text = block.get("question") or q.get("question") or ""
80
+ for c in CANONICAL_ORDER:
81
+ val = get_option_text_from_blocks(block, q, c)
82
+ if val:
83
+ options[c] = val
 
 
84
  else:
 
85
  question_text = str(block) if block else str(q.get("question", ""))
86
 
 
87
  if not options and isinstance(q, dict):
88
+ for c in CANONICAL_ORDER:
89
+ val = get_option_text_from_blocks({}, q, c)
90
+ if val:
91
+ options[c] = val
 
92
  return question_text.strip(), options, selected
93
 
94
  def flatten_entries(raw: Any) -> List[Dict[str, Any]]:
 
 
 
 
 
 
 
95
  out: List[Dict[str, Any]] = []
 
96
  def handle_item(obj: Dict[str, Any], default_name: str):
97
  q_text, opts, sel = _safe_get_question_block(obj)
 
98
  nm = (obj.get("name") or default_name or "Unknown").strip() or "Unknown"
99
  if q_text and opts and sel:
100
  out.append({"name": nm, "question": q_text, "options": opts, "selected": sel})
 
101
  if isinstance(raw, list):
102
  for x in raw:
103
  if isinstance(x, dict):
104
  handle_item(x, "Unknown")
105
  elif isinstance(raw, dict):
 
106
  for k, v in raw.items():
107
  default_name = str(k)
108
  if isinstance(v, list):
 
113
  handle_item(v, default_name)
114
  return out
115
 
116
+ def normalize_name(s: str) -> str:
117
+ return " ".join((s or "").strip().lower().split())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
+ def build_mismatch_list(entries: List[Dict[str, Any]], name_a: str, name_b: str):
120
+ A = normalize_name(name_a)
121
+ B = normalize_name(name_b)
122
+ # index by normalized question text
123
+ qmap: Dict[str, Dict[str, Dict[str, Any]]] = {}
124
  for e in entries:
125
+ q = " ".join(e["question"].split())
126
+ n = normalize_name(e["name"])
127
+ qmap.setdefault(q, {})[n] = e
128
+
129
+ mismatches = []
130
+ for q, per_name in qmap.items():
131
+ if A in per_name and B in per_name:
132
+ sel_a = per_name[A]["selected"]
133
+ sel_b = per_name[B]["selected"]
134
+ if sel_a != sel_b:
135
+ # prefer Eleanor's options, else Hung's
136
+ opts = per_name[A]["options"] or per_name[B]["options"]
137
+ mismatches.append({
138
+ "question": q,
139
+ "eleanor": per_name[A],
140
+ "hung": per_name[B],
141
+ "options": opts,
142
+ })
143
+ return mismatches
144
+
145
+ def make_display(item: Dict[str, Any], name_a_disp: str, name_b_disp: str) -> str:
146
+ q = item["question"]
147
+ sel_a = item["eleanor"]["selected"]
148
+ sel_b = item["hung"]["selected"]
149
+ opts = item["options"]
150
+
151
+ a_label = TRAIT_LABELS.get(sel_a, sel_a)
152
+ b_label = TRAIT_LABELS.get(sel_b, sel_b)
153
+
154
+ a_text = opts.get(sel_a, "")
155
+ b_text = opts.get(sel_b, "")
156
+
157
+ # styled spans
158
+ a_span = f"<span style='background:#e8ffe8;color:#0a6410;font-weight:700;'>{a_label}: {a_text}</span>"
159
+ b_span = f"<span style='background:#ffe8e8;color:#a00606;font-weight:700;'>{b_label}: {b_text}</span>"
160
+
161
+ body = [
162
+ f"### โ“ Question",
163
+ q,
164
+ "",
165
+ f"**{name_a_disp} chose:** {a_span}",
166
+ f"**{name_b_disp} chose:** {b_span}",
167
+ ]
168
+ return "\n\n".join(body)
169
 
170
+ DATA_RAW = load_json(DATA_PATH)
171
+ DATA = flatten_entries(DATA_RAW)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
 
173
+ with gr.Blocks(title="Eleanor vs Hung โ€” Differences Only") as demo:
174
+ gr.Markdown("# Eleanor vs Hung โ€” Different Answers Only")
175
+ gr.Markdown("Shows only the questions where the two personas chose different options.")
 
 
 
 
176
 
177
  with gr.Row():
178
+ name_a_in = gr.Textbox(value="Eleanor", label="Name A (green)", interactive=True)
179
+ name_b_in = gr.Textbox(value="Hung", label="Name B (red)", interactive=True)
180
  st_pos = gr.State(0)
181
 
182
  with gr.Row():
 
184
  next_btn = gr.Button("Next")
185
  rand_btn = gr.Button("Random")
186
 
187
+ status_md = gr.Markdown()
188
+ diff_md = gr.Markdown()
189
+
190
+ def recompute(name_a: str, name_b: str):
191
+ mismatches = build_mismatch_list(DATA, name_a, name_b)
192
+ total = len(mismatches)
193
+ if total == 0:
194
+ return 0, f"**0 differences** found for *{name_a}* vs *{name_b}*.", "_No differences to show._"
195
+ # show first
196
+ md = make_display(mismatches[0], name_a, name_b)
197
+ return 0, f"**{total} differences** found for *{name_a}* vs *{name_b}*.", md
198
+
199
+ def nav(name_a: str, name_b: str, pos: int, step: int = 0, rand: bool = False):
200
+ mismatches = build_mismatch_list(DATA, name_a, name_b)
201
+ total = len(mismatches)
202
+ if total == 0:
203
+ return pos, f"**0 differences** found for *{name_a}* vs *{name_b}*.", "_No differences to show._"
204
+ if rand:
205
+ pos = random.randrange(total)
206
+ else:
207
+ pos = (pos + step) % total
208
+ md = make_display(mismatches[pos], name_a, name_b)
209
+ return pos, f"**{total} differences** found โ€ข Showing {pos+1} / {total}", md
 
 
 
 
 
 
 
 
210
 
211
+ # Trigger recompute when names change
212
+ name_a_in.change(lambda a, b: recompute(a, b), inputs=[name_a_in, name_b_in], outputs=[st_pos, status_md, diff_md])
213
+ name_b_in.change(lambda a, b: recompute(a, b), inputs=[name_a_in, name_b_in], outputs=[st_pos, status_md, diff_md])
 
 
 
 
 
 
 
214
 
215
+ prev_btn.click(lambda a, b, p: nav(a, b, p, step=-1), inputs=[name_a_in, name_b_in, st_pos], outputs=[st_pos, status_md, diff_md])
216
+ next_btn.click(lambda a, b, p: nav(a, b, p, step=+1), inputs=[name_a_in, name_b_in, st_pos], outputs=[st_pos, status_md, diff_md])
217
+ rand_btn.click(lambda a, b, p: nav(a, b, p, rand=True), inputs=[name_a_in, name_b_in, st_pos], outputs=[st_pos, status_md, diff_md])
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
  # initial load
220
+ demo.load(lambda: recompute("Eleanor", "Hung"), inputs=None, outputs=[st_pos, status_md, diff_md])
 
 
 
 
221
 
222
  if __name__ == "__main__":
223
  demo.launch()
backup_app.py ADDED
@@ -0,0 +1,339 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # sjt_answers_viewer.py
3
+ # Gradio viewer for case_study_answers.json
4
+ # - Shows one question at a time
5
+ # - Dropdown 1: filter by **Name**
6
+ # - Dropdown 2: filter by **Selected Trait** (HEXACO slice)
7
+ # - Underlines & highlights the actually selected option + trait name
8
+ # - Always orders options consistently (HEXACO)
9
+ # - Top "Summary" bar shows proportion of selections by trait (under current Name filter)
10
+
11
+ import json
12
+ from pathlib import Path
13
+ from typing import List, Dict, Any, Optional, Tuple
14
+ import random
15
+
16
+ import gradio as gr
17
+ import matplotlib.pyplot as plt
18
+
19
+ # ---------- Constants ----------
20
+
21
+ DATA_PATH = Path("case_study_answers.json")
22
+
23
+ HEXACO_ORDER = [
24
+ "hh",
25
+ "emotionality",
26
+ "extraversion",
27
+ "agreeableness",
28
+ "conscientiousness",
29
+ "openness",
30
+ ]
31
+
32
+ TRAIT_LABELS = {
33
+ "hh": "Honestyโ€“Humility",
34
+ "emotionality": "Emotionality",
35
+ "extraversion": "Extraversion",
36
+ "agreeableness": "Agreeableness",
37
+ "conscientiousness": "Conscientiousness",
38
+ "openness": "Openness",
39
+ }
40
+
41
+ # ---------- Icons ----------
42
+
43
+ ICONS = {
44
+ "header": "๐Ÿ“",
45
+ "question": "โ“",
46
+ "options": "โœ…",
47
+ "summary": "๐Ÿ“Š",
48
+ "progress": "โญ๏ธ",
49
+ "metadata": "๐Ÿ”–",
50
+ "filters": "๐ŸŽ›๏ธ",
51
+ }
52
+
53
+ # ---------- Data Loading & Normalization ----------
54
+
55
+ def load_json(path: Path) -> Any:
56
+ if not path.exists():
57
+ return []
58
+ with path.open("r", encoding="utf-8") as f:
59
+ return json.load(f)
60
+
61
+ def _safe_get_question_block(item: Dict[str, Any]) -> Tuple[str, Dict[str, str], Optional[str]]:
62
+ """
63
+ Extract (question_text, options_map, selected_trait) from a raw item.
64
+ Heuristics:
65
+ - Selected trait is at top-level key 'option'.
66
+ - Question text/options may be under item['question'] with nested 'corrected_sjt' or 'original_sjt'.
67
+ - Options are expected as keys like '<trait>_option' where trait โˆˆ HEXACO_ORDER.
68
+ """
69
+ selected = item.get("option")
70
+
71
+ q = item.get("question", {}) or {}
72
+ block = q.get("corrected_sjt") or q.get("original_sjt") or {}
73
+
74
+ question_text = ""
75
+ options: Dict[str, str] = {}
76
+
77
+ if isinstance(block, dict):
78
+ question_text = block.get("question") or q.get("question") or ""
79
+ for trait in HEXACO_ORDER:
80
+ k = f"{trait}_option"
81
+ if k in block:
82
+ options[trait] = str(block[k]).strip()
83
+ elif k in q:
84
+ options[trait] = str(q[k]).strip()
85
+ else:
86
+ # sometimes block is a plain string
87
+ question_text = str(block) if block else str(q.get("question", ""))
88
+
89
+ # Fallback: look for options directly on item if missing
90
+ if not options and isinstance(q, dict):
91
+ for trait in HEXACO_ORDER:
92
+ k = f"{trait}_option"
93
+ if k in q:
94
+ options[trait] = str(q[k]).strip()
95
+
96
+ return question_text.strip(), options, selected
97
+
98
+ def flatten_entries(raw: Any) -> List[Dict[str, Any]]:
99
+ """
100
+ Returns a list of entries with keys:
101
+ - name (str)
102
+ - question (str)
103
+ - options (dict[trait->text])
104
+ - selected (trait str)
105
+ """
106
+ out: List[Dict[str, Any]] = []
107
+
108
+ def handle_item(obj: Dict[str, Any], default_name: str):
109
+ q_text, opts, sel = _safe_get_question_block(obj)
110
+ # Prefer name from object if present; else inherit from container
111
+ nm = (obj.get("name") or default_name or "Unknown").strip() or "Unknown"
112
+ if q_text and opts and sel:
113
+ out.append({"name": nm, "question": q_text, "options": opts, "selected": sel})
114
+
115
+ if isinstance(raw, list):
116
+ for x in raw:
117
+ if isinstance(x, dict):
118
+ handle_item(x, "Unknown")
119
+ elif isinstance(raw, dict):
120
+ # Could be {persona_name: [items]} or {persona_name: {...}} etc.
121
+ for k, v in raw.items():
122
+ default_name = str(k)
123
+ if isinstance(v, list):
124
+ for x in v:
125
+ if isinstance(x, dict):
126
+ handle_item(x, default_name)
127
+ elif isinstance(v, dict):
128
+ handle_item(v, default_name)
129
+ return out
130
+
131
+ DATA_RAW = load_json(DATA_PATH)
132
+ DATA: List[Dict[str, Any]] = flatten_entries(DATA_RAW)
133
+
134
+ # Unique names for dropdown
135
+ def all_names(entries: List[Dict[str, Any]]) -> List[str]:
136
+ seen = []
137
+ for e in entries:
138
+ nm = e.get("name", "Unknown") or "Unknown"
139
+ if nm not in seen:
140
+ seen.append(nm)
141
+ return sorted(seen)
142
+
143
+ NAME_FILTERS = ["All"] + all_names(DATA)
144
+ TRAIT_FILTERS = ["All"] + HEXACO_ORDER
145
+
146
+ # ---------- Filtering & Navigation ----------
147
+
148
+ def get_filtered_indices(entries: List[Dict[str, Any]], name_filt: str, trait_filt: str) -> List[int]:
149
+ idxs = list(range(len(entries)))
150
+ if name_filt != "All":
151
+ idxs = [i for i in idxs if entries[i].get("name") == name_filt]
152
+ if trait_filt != "All":
153
+ idxs = [i for i in idxs if entries[i].get("selected") == trait_filt]
154
+ return idxs
155
+
156
+ def clamp_index(i: int, n: int) -> int:
157
+ return 0 if n == 0 else (i % n)
158
+
159
+ # ---------- Summary ----------
160
+
161
+ def compute_summary(entries: List[Dict[str, Any]]):
162
+ total = len(entries)
163
+ counts = {t: 0 for t in HEXACO_ORDER}
164
+ for e in entries:
165
+ sel = e.get("selected")
166
+ if sel in counts:
167
+ counts[sel] += 1
168
+ labels = [TRAIT_LABELS[t] for t in HEXACO_ORDER]
169
+ props = [counts[t] / total if total else 0.0 for t in HEXACO_ORDER]
170
+ return labels, props, counts, total
171
+
172
+ def summary_plot(entries: List[Dict[str, Any]]):
173
+ # Returns Markdown with proportions per trait under the current Name filter
174
+ labels, props, counts, total = compute_summary(entries)
175
+ lines = ["## ๐Ÿ“Š Summary (Name filter)", f"**Total:** {total}"]
176
+ for label, p in zip(labels, props):
177
+ lines.append(f"- {label}: {p:.2f}")
178
+ return "\n".join(lines)
179
+
180
+ # ---------- Rendering ----------
181
+
182
+ def md_question(entry: Dict[str, Any]) -> str:
183
+ q = entry.get("question", "")
184
+ name = entry.get("name", "โ€”")
185
+ return f"## {ICONS['question']} Question\n**Name:** {name}\n\n{q if q else 'โ€”'}"
186
+
187
+ def md_options(entry: Dict[str, Any]) -> str:
188
+ opts: Dict[str, str] = entry.get("options", {})
189
+ selected = entry.get("selected")
190
+ lines = []
191
+ for i, trait in enumerate(HEXACO_ORDER, start=1):
192
+ if trait not in opts:
193
+ continue
194
+ label = TRAIT_LABELS[trait]
195
+ text = opts[trait]
196
+ if trait == selected:
197
+ # underline + highlight both the label and the text
198
+ line = (
199
+ f"{i}. <u><mark><strong>{label}</strong>:</mark></u> "
200
+ f"<u><mark>{text}</mark></u>"
201
+ )
202
+ else:
203
+ line = f"{i}. **{label}:** {text}"
204
+ lines.append(line)
205
+ body = "\n\n".join(lines) if lines else "โ€”"
206
+ return f"## {ICONS['options']} Options (HEXACO order)\n{body}"
207
+
208
+ def md_metadata(entry: Dict[str, Any], idx: int, total_in_filter: int) -> str:
209
+ sel = entry.get("selected", "โ€”")
210
+ sel_disp = TRAIT_LABELS.get(sel, sel)
211
+ nm = entry.get("name", "โ€”")
212
+ return (
213
+ f"## {ICONS['metadata']} Metadata\n"
214
+ f"**Name:** {nm} \n"
215
+ f"**Selected Option (Trait):** {sel_disp} \n"
216
+ f"**Position in Filter:** {idx + 1} / {total_in_filter}"
217
+ )
218
+
219
+ def md_progress(idx: int, total: int) -> str:
220
+ return f"## {ICONS['progress']} Progress\n**{idx + 1} / {total}**"
221
+
222
+ def render(entries: List[Dict[str, Any]], name_filt: str, trait_filt: str, pos: int):
223
+ # For summary, use "name-only" filter to show that persona's distribution
224
+ name_only_indices = [i for i, e in enumerate(entries) if (name_filt == "All" or e.get("name") == name_filt)]
225
+ name_only_slice = [entries[i] for i in name_only_indices]
226
+
227
+ # For the main view selection, apply both filters
228
+ indices = get_filtered_indices(entries, name_filt, trait_filt)
229
+ n = len(indices)
230
+
231
+ if n == 0:
232
+ return (
233
+ summary_plot(name_only_slice),
234
+ f"## {ICONS['question']} Question\n_No questions for filters **Name={name_filt}**, **Trait={trait_filt}**._",
235
+ f"## {ICONS['options']} Options\nโ€”",
236
+ f"## {ICONS['metadata']} Metadata\nโ€”",
237
+ f"## {ICONS['progress']} Progress\n0 / 0",
238
+ 0, # expose pos back
239
+ )
240
+
241
+ pos = clamp_index(pos, n)
242
+ entry = entries[indices[pos]]
243
+ return (
244
+ summary_plot(name_only_slice),
245
+ md_question(entry),
246
+ md_options(entry),
247
+ md_metadata(entry, pos, n),
248
+ md_progress(pos, n),
249
+ pos, # expose pos back
250
+ )
251
+
252
+ # ---------- Gradio App ----------
253
+
254
+ with gr.Blocks(title="SJT Answers Viewer") as demo:
255
+ gr.Markdown("# SJT Answers Viewer")
256
+ gr.Markdown(
257
+ f"{ICONS['filters']} **Filters:** Choose a Name and a HEXACO Selected-Trait slice.\n\n"
258
+ f"{ICONS['summary']} **Summary:** Bar shows the trait-selection proportions under the current **Name** filter.\n\n"
259
+ "Options are consistently ordered by HEXACO. The actual selected option is underlined and highlighted."
260
+ )
261
+
262
+ with gr.Row():
263
+ name_dd = gr.Dropdown(choices=NAME_FILTERS, value="All", label="Filter by Name", interactive=True)
264
+ trait_dd = gr.Dropdown(choices=TRAIT_FILTERS, value="All", label="Filter by Selected Trait", interactive=True)
265
+ st_pos = gr.State(0)
266
+
267
+ with gr.Row():
268
+ prev_btn = gr.Button("Previous")
269
+ next_btn = gr.Button("Next")
270
+ rand_btn = gr.Button("Random")
271
+
272
+ # Outputs
273
+ summary_out = gr.Markdown(label="Selections Summary (Name filter)")
274
+ question_out = gr.Markdown()
275
+ options_out = gr.Markdown()
276
+ metadata_out = gr.Markdown()
277
+ progress_out = gr.Markdown()
278
+
279
+ # ----- Callbacks -----
280
+ def on_filters_change(name_filt: str, trait_filt: str):
281
+ return [*render(DATA, name_filt, trait_filt, 0), 0]
282
+
283
+ def on_prev(name_filt: str, trait_filt: str, pos: int):
284
+ indices = get_filtered_indices(DATA, name_filt, trait_filt)
285
+ if not indices:
286
+ return [*render(DATA, name_filt, trait_filt, pos), pos]
287
+ pos = clamp_index(pos - 1, len(indices))
288
+ return [*render(DATA, name_filt, trait_filt, pos), pos]
289
+
290
+ def on_next(name_filt: str, trait_filt: str, pos: int):
291
+ indices = get_filtered_indices(DATA, name_filt, trait_filt)
292
+ if not indices:
293
+ return [*render(DATA, name_filt, trait_filt, pos), pos]
294
+ pos = clamp_index(pos + 1, len(indices))
295
+ return [*render(DATA, name_filt, trait_filt, pos), pos]
296
+
297
+ def on_rand(name_filt: str, trait_filt: str, pos: int):
298
+ indices = get_filtered_indices(DATA, name_filt, trait_filt)
299
+ if not indices:
300
+ return [*render(DATA, name_filt, trait_filt, pos), pos]
301
+ pos = random.randrange(len(indices))
302
+ return [*render(DATA, name_filt, trait_filt, pos), pos]
303
+
304
+ name_dd.change(
305
+ on_filters_change,
306
+ inputs=[name_dd, trait_dd],
307
+ outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
308
+ )
309
+ trait_dd.change(
310
+ on_filters_change,
311
+ inputs=[name_dd, trait_dd],
312
+ outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
313
+ )
314
+
315
+ prev_btn.click(
316
+ on_prev,
317
+ inputs=[name_dd, trait_dd, st_pos],
318
+ outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
319
+ )
320
+ next_btn.click(
321
+ on_next,
322
+ inputs=[name_dd, trait_dd, st_pos],
323
+ outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
324
+ )
325
+ rand_btn.click(
326
+ on_rand,
327
+ inputs=[name_dd, trait_dd, st_pos],
328
+ outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
329
+ )
330
+
331
+ # initial load
332
+ demo.load(
333
+ lambda: [*render(DATA, "All", "All", 0), 0],
334
+ inputs=None,
335
+ outputs=[summary_out, question_out, options_out, metadata_out, progress_out, st_pos, st_pos],
336
+ )
337
+
338
+ if __name__ == "__main__":
339
+ demo.launch()