Sina1138 commited on
Commit
8ea388a
ยท
1 Parent(s): d8b84f2

Add Expand/Collapse functionality for reviews and rebuttals, enhance HTML rendering for improved formatting, default to light mode

Browse files
Files changed (1) hide show
  1. interface/Demo.py +257 -88
interface/Demo.py CHANGED
@@ -40,23 +40,163 @@ def _get_context(sentence: str, sentence_lists: list):
40
  return "", ""
41
 
42
 
 
 
 
 
 
 
43
  def _rebuttal_toggle_html() -> str:
44
  """Generate an Expand/Collapse All Responses toggle button with inline JS."""
45
  return (
46
- '<div style="display:flex;justify-content:flex-end;margin-bottom:4px;">'
47
  '<button onclick="'
48
  "let tab=this.closest('.tabitem')||this.closest('.gradio-container');"
49
- "let details=tab.querySelectorAll('details');"
 
50
  "let allOpen=Array.from(details).every(d=>d.open);"
51
  "details.forEach(d=>d.open=!allOpen);"
52
  "this.textContent=allOpen?'Expand All Responses':'Collapse All Responses';"
53
- '" style="'
54
- 'background:none;border:1px solid #d1d5db;border-radius:6px;padding:4px 12px;'
55
- 'font-size:0.78em;color:#6b7280;cursor:pointer;white-space:nowrap;'
56
- '">Expand All Responses</button></div>'
57
  )
58
 
59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  def format_summary_cards(
61
  sentences: list,
62
  scores: dict,
@@ -287,6 +427,7 @@ def render_agreement_html(
287
  speaker: Dict[str, Dict[str, float]],
288
  num_reviews: int,
289
  label: str = "Agreement",
 
290
  ) -> str:
291
  """
292
  Custom HTML renderer for Agreement mode (replaces gr.HighlightedText).
@@ -310,21 +451,39 @@ def render_agreement_html(
310
  '</div>'
311
  )
312
 
313
- parts = [f'<div style="font-weight:600;font-size:0.85em;color:#374151;margin-bottom:4px;">{_html.escape(label)}</div>']
 
 
 
 
 
 
 
 
 
 
 
314
  parts.append(legend_html)
315
- parts.append('<div style="line-height:1.8;font-size:0.95em;">')
316
 
317
  # Compute informativeness threshold: 2 / K (twice uniform baseline)
318
  k = max(len(uniqueness), 1)
319
  info_threshold = 2.0 / k
320
 
321
- for sent in sentences:
 
 
 
 
322
  sent_id = _make_sentence_id(sent)
323
  score = uniqueness.get(sent)
324
 
 
 
 
325
  if score is None or abs(score) < HIGHLIGHT_THRESHOLD:
326
  # No highlight
327
- parts.append(f'<span id="{sent_id}">{_html.escape(sent)} </span>')
328
  continue
329
 
330
  # --- Color and opacity ---
@@ -388,6 +547,8 @@ def render_agreement_html(
388
  )
389
 
390
  parts.append("</div>")
 
 
391
  return "".join(parts)
392
 
393
 
@@ -963,7 +1124,12 @@ details summary::-webkit-details-marker { display: none; }
963
  details[open] summary span:first-child { display: inline-block; transform: rotate(90deg); }
964
  """
965
 
966
- with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
 
 
 
 
 
967
  # gr.Markdown("# ReView Interface")
968
 
969
  # TODO: Uncomment this for home/description tab once finished with testing.
@@ -1121,61 +1287,58 @@ with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
1121
  if i < number_of_displayed_reviews:
1122
  review_item, rebuttal_html = review_items_cache[i]
1123
 
1124
- if show_polarity:
1125
- highlighted = []
1126
- for sentence, metadata in review_item:
1127
- polarity = metadata.get("polarity", None)
1128
- if polarity == 2:
1129
- label = "โž•"
1130
- elif polarity == 0:
1131
- label = "โž–"
1132
- else:
1133
- label = None
1134
- highlighted.append((sentence, label))
1135
- elif show_consensuality:
1136
- highlighted = [(sentence, None) for sentence, _ in review_item]
1137
- elif show_topic:
1138
- highlighted = []
1139
- for sentence, metadata in review_item:
1140
- topic = metadata.get("topic", None)
1141
- if topic != "NONE":
1142
- highlighted.append((sentence, topic))
1143
- else:
1144
- highlighted.append((sentence, None))
1145
- else:
1146
- highlighted = [
1147
- (sentence, None)
1148
- for sentence, _ in review_item
1149
- ]
1150
-
1151
- # HighlightedText: visible for all modes except agreement
1152
  review_updates.append(
1153
  gr.update(
1154
- visible=not show_consensuality,
1155
- value=highlighted,
 
1156
  color_map=color_map,
1157
- show_legend=legend,
1158
  key=f"updated_{score_type}_{i}"
1159
  )
1160
  )
1161
 
1162
- # Agreement HTML: visible only in agreement mode
1163
  if show_consensuality:
1164
  sentences_for_review = [s for s, _ in review_item]
1165
- agreement_html = render_agreement_html(
1166
  sentences_for_review, consensuality_dict,
1167
  listener=prep_listener, speaker=prep_speaker,
1168
  num_reviews=number_of_displayed_reviews,
1169
- label=f"Agreement in Review {i + 1}",
1170
  )
1171
  # Append per-review divergent cards (if RSA data available)
1172
  if i in divergent_per_review:
1173
- agreement_html += divergent_per_review[i]
1174
- agreement_updates.append(gr.update(visible=True, value=agreement_html))
 
 
 
 
 
 
 
1175
  else:
1176
- agreement_updates.append(gr.update(visible=False, value=""))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1177
 
1178
- rebuttal_updates.append(gr.update(visible=bool(rebuttal_html), value=rebuttal_html))
 
 
1179
  else:
1180
  review_updates.append(
1181
  gr.update(
@@ -1225,33 +1388,44 @@ with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
1225
  most_common_visibility = gr.update(visible=False, value="")
1226
  most_unique_visibility = gr.update(visible=False, value="")
1227
 
1228
- # update topic color map
1229
- if show_topic:
1230
- topic_color_map_visibility = gr.update(
1231
- visible=True,
1232
- color_map=topic_color_map,
1233
- value=[
1234
- ("", "Substance"),
1235
- ("", "Clarity"),
1236
- ("", "Soundness/Correctness"),
1237
- ("", "Originality"),
1238
- ("", "Motivation/Impact"),
1239
- ("", "Meaningful Comparison"),
1240
- ("", "Replicability"),
1241
- ]
 
1242
  )
 
 
 
 
 
1243
  else:
1244
- topic_color_map_visibility = gr.update(visible=False, value=[])
1245
 
1246
- # Toggle button for expanding/collapsing all author responses
1247
  has_any_rebuttal = any(
1248
- r.get("value", "") for r in rebuttal_updates
1249
- if isinstance(r, dict)
1250
  )
 
1251
  if has_any_rebuttal:
1252
- rebuttal_toggle_update = gr.update(visible=True, value=_rebuttal_toggle_html())
1253
- else:
1254
- rebuttal_toggle_update = gr.update(visible=False, value="")
 
 
 
 
1255
 
1256
  return (
1257
  new_review_id,
@@ -1260,7 +1434,7 @@ with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
1260
  most_common_visibility,
1261
  most_unique_visibility,
1262
  topic_color_map_visibility,
1263
- rebuttal_toggle_update, # Toggle button
1264
  *rebuttal_updates, # 10 per-review rebuttals
1265
  general_rebuttal_update, # General rebuttal section
1266
  state
@@ -1304,20 +1478,12 @@ with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
1304
  label="Most Divergent Opinions",
1305
  )
1306
 
1307
- # Add a new textbox for topic labels and colors
1308
- topic_text_box = gr.HighlightedText(
1309
- label="Topic Labels (Color-Coded)",
1310
- visible=False,
1311
- value=[],
1312
- show_legend=True,
1313
- )
1314
 
1315
  with gr.Row():
1316
  gr.Markdown("### ๐Ÿ“ Reviews", elem_classes=["review-section-header"])
1317
- prep_rebuttal_toggle = gr.HTML(
1318
- visible=False, value="",
1319
- elem_classes=["rebuttal-toggle-container"],
1320
- )
1321
  review1 = gr.HighlightedText(show_legend=False, label="๐Ÿ“ Review 1", visible=number_of_displayed_reviews >= 1, key="initial_review1")
1322
  prep_agreement1 = gr.HTML(visible=False, value="")
1323
  prep_rebuttal1 = gr.HTML(visible=False, value="")
@@ -1373,7 +1539,7 @@ with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
1373
  return update_review_display(state, score_type)
1374
 
1375
  # Hook up the callbacks with the session state.
1376
- _review_outputs = [review_id, review1, review2, review3, review4, review5, review6, review7, review8, review9, review10, prep_agreement1, prep_agreement2, prep_agreement3, prep_agreement4, prep_agreement5, prep_agreement6, prep_agreement7, prep_agreement8, prep_agreement9, prep_agreement10, most_common_sentences, most_unique_sentences, topic_text_box, prep_rebuttal_toggle, prep_rebuttal1, prep_rebuttal2, prep_rebuttal3, prep_rebuttal4, prep_rebuttal5, prep_rebuttal6, prep_rebuttal7, prep_rebuttal8, prep_rebuttal9, prep_rebuttal10, prep_general_rebuttal, state]
1377
  year.change(fn=year_change, inputs=[year, state, score_type], outputs=_review_outputs)
1378
  score_type.change(fn=update_review_display, inputs=[state, score_type], outputs=_review_outputs)
1379
  next_button.click(fn=next_review, inputs=[state, score_type], outputs=_review_outputs)
@@ -1533,13 +1699,16 @@ with gr.Blocks(title="ReView", css=CUSTOM_CSS) as demo:
1533
  general_formatted = format_general_rebuttals(rebuttal or "")
1534
  has_general = bool(general_formatted)
1535
 
1536
- # Show toggle if any rebuttals exist
1537
  has_any = any(bool(format_rebuttal_for_review(rebuttal or "", i)) for i in range(1, 7)) or has_general
 
1538
  if has_any:
1539
- toggle_html = _rebuttal_toggle_html()
1540
- toggle_update = gr.update(visible=True, value=toggle_html)
1541
- else:
1542
- toggle_update = gr.update(visible=False, value="")
 
 
1543
 
1544
  return (
1545
  gr.update(visible=False), # input_section
 
40
  return "", ""
41
 
42
 
43
+ _TOGGLE_BTN_STYLE = (
44
+ 'background:none;border:1px solid #d1d5db;border-radius:6px;padding:4px 12px;'
45
+ 'font-size:0.78em;color:#6b7280;cursor:pointer;white-space:nowrap;'
46
+ 'line-height:1.4;height:28px;box-sizing:border-box;'
47
+ )
48
+
49
  def _rebuttal_toggle_html() -> str:
50
  """Generate an Expand/Collapse All Responses toggle button with inline JS."""
51
  return (
 
52
  '<button onclick="'
53
  "let tab=this.closest('.tabitem')||this.closest('.gradio-container');"
54
+ "let details=tab.querySelectorAll('details:not(.review-collapse)');"
55
+ "if(!details.length)return;"
56
  "let allOpen=Array.from(details).every(d=>d.open);"
57
  "details.forEach(d=>d.open=!allOpen);"
58
  "this.textContent=allOpen?'Expand All Responses':'Collapse All Responses';"
59
+ f'" style="{_TOGGLE_BTN_STYLE}"'
60
+ '>Expand All Responses</button>'
 
 
61
  )
62
 
63
 
64
+ def _review_toggle_html() -> str:
65
+ """Generate a Collapse/Expand All Reviews toggle button with inline JS."""
66
+ return (
67
+ '<button onclick="'
68
+ "let tab=this.closest('.tabitem')||this.closest('.gradio-container');"
69
+ "let details=tab.querySelectorAll('details.review-collapse');"
70
+ "if(!details.length)return;"
71
+ "let allOpen=Array.from(details).every(d=>d.open);"
72
+ "details.forEach(d=>d.open=!allOpen);"
73
+ "this.textContent=allOpen?'Expand All Reviews':'Collapse All Reviews';"
74
+ f'" style="{_TOGGLE_BTN_STYLE}"'
75
+ '>Collapse All Reviews</button>'
76
+ )
77
+
78
+
79
+ import re as _re
80
+
81
+ def _should_break_before(sent: str) -> bool:
82
+ """Detect if a paragraph break should be inserted before this sentence."""
83
+ s = sent.strip()
84
+ # Numbered items: 1), 2., (1), 1:, etc.
85
+ if _re.match(r'^[\(\[]?\d+[\)\]\.:]', s):
86
+ return True
87
+ # Dash/bullet items
88
+ if len(s) > 2 and s[0] in ('-', 'โ€ข', '*', 'โ€“', 'โ€”') and s[1] == ' ':
89
+ return True
90
+ # Markdown separators / headers
91
+ if s.startswith('##') or s.startswith('---'):
92
+ return True
93
+ # Common review section headers
94
+ if _re.match(
95
+ r'^\*{0,2}(Rating|Strengths?|Weaknesses?|Questions?|Limitations?|Summary|'
96
+ r'Soundness|Presentation|Contribution|Confidence|Experience|Review Assessment|'
97
+ r'Recommendation|Overall|Minor|Major|Typos?|Suggestions?|Comments?|'
98
+ r'Detailed\s+Comments?|Pros?|Cons?|Flag|Clarity|Significance|Originality)',
99
+ s, _re.IGNORECASE,
100
+ ):
101
+ return True
102
+ return False
103
+
104
+
105
+ def _is_review_header(sent: str) -> bool:
106
+ """Detect if a sentence is a review metadata header (Rating:, Experience:, etc.)."""
107
+ return bool(_re.match(
108
+ r'^\*{0,2}(Rating|Confidence|Experience|Review Assessment|Recommendation|Flag)\b',
109
+ sent.strip(), _re.IGNORECASE,
110
+ ))
111
+
112
+
113
+ # ---- Polarity / Topic color maps for HTML rendering ----
114
+ _POLARITY_COLORS = {2: "#d4fcd6", 0: "#fcd6d6"} # positive=green, negative=red
115
+ _TOPIC_HTML_COLORS = {
116
+ "Substance": "#b3e5fc",
117
+ "Clarity": "#c8e6c9",
118
+ "Soundness/Correctness": "#fff9c4",
119
+ "Originality": "#f8bbd0",
120
+ "Motivation/Impact": "#d1c4e9",
121
+ "Meaningful Comparison": "#ffe0b2",
122
+ "Replicability": "#b2dfdb",
123
+ }
124
+
125
+
126
+ def render_review_html(
127
+ review_items: list,
128
+ mode: str = "plain",
129
+ label: str = "Review",
130
+ wrap: bool = False,
131
+ ) -> str:
132
+ """
133
+ Render a review as HTML with proper paragraph formatting.
134
+
135
+ Args:
136
+ review_items: list of (sentence, metadata_dict) tuples
137
+ mode: "plain", "polarity", or "topic"
138
+ label: header label
139
+ wrap: if False, return bare content (caller handles outer wrapper)
140
+ """
141
+ if not review_items:
142
+ return ""
143
+
144
+ parts = []
145
+ if wrap:
146
+ parts.append(
147
+ '<details open class="review-collapse" style="border:1px solid #e5e7eb;border-radius:8px;padding:12px 16px;margin-bottom:10px;">'
148
+ f'<summary style="font-weight:600;font-size:0.85em;color:#374151;cursor:pointer;'
149
+ f'list-style:none;display:flex;align-items:center;gap:6px;">'
150
+ f'<span style="transition:transform 0.2s;font-size:0.7em;">โ–ถ</span> '
151
+ f'{_html.escape(label)}</summary>'
152
+ )
153
+ else:
154
+ if label:
155
+ parts.append(f'<div style="font-weight:600;font-size:0.85em;color:#374151;margin-bottom:4px;">{_html.escape(label)}</div>')
156
+ parts.append('<div style="line-height:1.8;font-size:0.95em;margin-top:6px;">')
157
+
158
+ for i, (sent, metadata) in enumerate(review_items):
159
+ # Paragraph break detection
160
+ if i > 0 and _should_break_before(sent):
161
+ parts.append('<br>')
162
+
163
+ # Header styling (Rating:, Experience:, etc.)
164
+ is_header = _is_review_header(sent)
165
+
166
+ bg = ""
167
+ label_text = ""
168
+ if mode == "polarity":
169
+ polarity = metadata.get("polarity")
170
+ if polarity in _POLARITY_COLORS:
171
+ bg = f"background:{_POLARITY_COLORS[polarity]};"
172
+ elif mode == "topic":
173
+ topic = metadata.get("topic")
174
+ if topic and topic != "NONE" and topic in _TOPIC_HTML_COLORS:
175
+ bg = f"background:{_TOPIC_HTML_COLORS[topic]};"
176
+ label_text = topic
177
+
178
+ style = f"padding:1px 3px;border-radius:3px;{bg}"
179
+ if is_header:
180
+ style += "font-weight:600;color:#92400e;"
181
+
182
+ sent_id = _make_sentence_id(sent)
183
+ escaped = _html.escape(sent)
184
+
185
+ if label_text:
186
+ # Show topic label as a small tag
187
+ parts.append(
188
+ f'<span id="{sent_id}" style="{style}" title="{_html.escape(label_text)}">'
189
+ f'{escaped} </span>'
190
+ )
191
+ else:
192
+ parts.append(f'<span id="{sent_id}" style="{style}">{escaped} </span>')
193
+
194
+ parts.append('</div>')
195
+ if wrap:
196
+ parts.append('</details>')
197
+ return "".join(parts)
198
+
199
+
200
  def format_summary_cards(
201
  sentences: list,
202
  scores: dict,
 
427
  speaker: Dict[str, Dict[str, float]],
428
  num_reviews: int,
429
  label: str = "Agreement",
430
+ wrap: bool = False,
431
  ) -> str:
432
  """
433
  Custom HTML renderer for Agreement mode (replaces gr.HighlightedText).
 
451
  '</div>'
452
  )
453
 
454
+ parts = []
455
+ if wrap:
456
+ parts.append(
457
+ '<details open class="review-collapse" style="border:1px solid #e5e7eb;border-radius:8px;padding:12px 16px;margin-bottom:10px;">'
458
+ f'<summary style="font-weight:600;font-size:0.85em;color:#374151;cursor:pointer;'
459
+ f'list-style:none;display:flex;align-items:center;gap:6px;">'
460
+ f'<span style="transition:transform 0.2s;font-size:0.7em;">โ–ถ</span> '
461
+ f'{_html.escape(label)}</summary>'
462
+ )
463
+ else:
464
+ if label:
465
+ parts.append(f'<div style="font-weight:600;font-size:0.85em;color:#374151;margin-bottom:4px;">{_html.escape(label)}</div>')
466
  parts.append(legend_html)
467
+ parts.append('<div style="line-height:1.8;font-size:0.95em;margin-top:6px;">')
468
 
469
  # Compute informativeness threshold: 2 / K (twice uniform baseline)
470
  k = max(len(uniqueness), 1)
471
  info_threshold = 2.0 / k
472
 
473
+ for idx, sent in enumerate(sentences):
474
+ # Paragraph break detection
475
+ if idx > 0 and _should_break_before(sent):
476
+ parts.append('<br>')
477
+
478
  sent_id = _make_sentence_id(sent)
479
  score = uniqueness.get(sent)
480
 
481
+ # Header styling (Rating:, Experience:, etc.)
482
+ header_style = "font-weight:600;color:#92400e;" if _is_review_header(sent) else ""
483
+
484
  if score is None or abs(score) < HIGHLIGHT_THRESHOLD:
485
  # No highlight
486
+ parts.append(f'<span id="{sent_id}" style="{header_style}">{_html.escape(sent)} </span>')
487
  continue
488
 
489
  # --- Color and opacity ---
 
547
  )
548
 
549
  parts.append("</div>")
550
+ if wrap:
551
+ parts.append("</details>")
552
  return "".join(parts)
553
 
554
 
 
1124
  details[open] summary span:first-child { display: inline-block; transform: rotate(90deg); }
1125
  """
1126
 
1127
+ with gr.Blocks(
1128
+ title="ReView",
1129
+ css=CUSTOM_CSS,
1130
+ theme=gr.themes.Default(),
1131
+ js="() => { document.querySelector('body').classList.remove('dark'); }",
1132
+ ) as demo:
1133
  # gr.Markdown("# ReView Interface")
1134
 
1135
  # TODO: Uncomment this for home/description tab once finished with testing.
 
1287
  if i < number_of_displayed_reviews:
1288
  review_item, rebuttal_html = review_items_cache[i]
1289
 
1290
+ # All modes now use HTML rendering for proper paragraph formatting.
1291
+ # HighlightedText is always hidden; prep_agreement HTML is always shown.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1292
  review_updates.append(
1293
  gr.update(
1294
+ visible=False,
1295
+ value=[],
1296
+ show_legend=False,
1297
  color_map=color_map,
 
1298
  key=f"updated_{score_type}_{i}"
1299
  )
1300
  )
1301
 
1302
+ review_label = f"Review {i + 1}"
1303
  if show_consensuality:
1304
  sentences_for_review = [s for s, _ in review_item]
1305
+ inner_html = render_agreement_html(
1306
  sentences_for_review, consensuality_dict,
1307
  listener=prep_listener, speaker=prep_speaker,
1308
  num_reviews=number_of_displayed_reviews,
1309
+ label="",
1310
  )
1311
  # Append per-review divergent cards (if RSA data available)
1312
  if i in divergent_per_review:
1313
+ inner_html += divergent_per_review[i]
1314
+ elif show_polarity:
1315
+ inner_html = render_review_html(
1316
+ review_item, mode="polarity", label="",
1317
+ )
1318
+ elif show_topic:
1319
+ inner_html = render_review_html(
1320
+ review_item, mode="topic", label="",
1321
+ )
1322
  else:
1323
+ inner_html = render_review_html(
1324
+ review_item, mode="plain", label="",
1325
+ )
1326
+
1327
+ # Wrap review content + rebuttal in a single collapsible card
1328
+ html_content = (
1329
+ '<details open class="review-collapse" style="border:1px solid #e5e7eb;'
1330
+ 'border-radius:8px;padding:12px 16px;margin-bottom:10px;">'
1331
+ f'<summary style="font-weight:600;font-size:0.9em;color:#374151;cursor:pointer;'
1332
+ f'list-style:none;display:flex;align-items:center;gap:6px;">'
1333
+ f'<span style="transition:transform 0.2s;font-size:0.7em;">โ–ถ</span> '
1334
+ f'{_html.escape(review_label)}</summary>'
1335
+ f'{inner_html}{rebuttal_html}'
1336
+ '</details>'
1337
+ )
1338
 
1339
+ agreement_updates.append(gr.update(visible=True, value=html_content))
1340
+ # Rebuttal is now embedded in the review card, so hide the separate component
1341
+ rebuttal_updates.append(gr.update(visible=False, value=""))
1342
  else:
1343
  review_updates.append(
1344
  gr.update(
 
1388
  most_common_visibility = gr.update(visible=False, value="")
1389
  most_unique_visibility = gr.update(visible=False, value="")
1390
 
1391
+ # update color legend (topic or polarity)
1392
+ if show_polarity:
1393
+ polarity_legend = (
1394
+ '<div style="display:flex;gap:12px;align-items:center;padding:8px 0;font-size:0.8em;">'
1395
+ '<span style="background:#d4fcd6;padding:2px 8px;border-radius:4px;">Positive</span>'
1396
+ '<span style="background:#fcd6d6;padding:2px 8px;border-radius:4px;">Negative</span>'
1397
+ '<span style="color:#9ca3af;">Neutral (no highlight)</span>'
1398
+ '</div>'
1399
+ )
1400
+ topic_color_map_visibility = gr.update(visible=True, value=polarity_legend)
1401
+ elif show_topic:
1402
+ legend_items = " ".join(
1403
+ f'<span style="background:{color};padding:2px 8px;border-radius:4px;'
1404
+ f'font-size:0.8em;margin-right:4px;">{_html.escape(name)}</span>'
1405
+ for name, color in _TOPIC_HTML_COLORS.items()
1406
  )
1407
+ topic_legend_html = (
1408
+ f'<div style="display:flex;flex-wrap:wrap;gap:4px;align-items:center;'
1409
+ f'padding:8px 0;">{legend_items}</div>'
1410
+ )
1411
+ topic_color_map_visibility = gr.update(visible=True, value=topic_legend_html)
1412
  else:
1413
+ topic_color_map_visibility = gr.update(visible=False, value="")
1414
 
1415
+ # Toggle bar: review collapse + rebuttal expand buttons
1416
  has_any_rebuttal = any(
1417
+ rebuttal_html
1418
+ for _, rebuttal_html in review_items_cache
1419
  )
1420
+ toggle_buttons = [_review_toggle_html()]
1421
  if has_any_rebuttal:
1422
+ toggle_buttons.append(_rebuttal_toggle_html())
1423
+ toggle_bar_html = (
1424
+ '<div style="display:flex;justify-content:flex-end;gap:8px;">'
1425
+ + "".join(toggle_buttons)
1426
+ + '</div>'
1427
+ )
1428
+ toggle_bar_update = gr.update(visible=True, value=toggle_bar_html)
1429
 
1430
  return (
1431
  new_review_id,
 
1434
  most_common_visibility,
1435
  most_unique_visibility,
1436
  topic_color_map_visibility,
1437
+ toggle_bar_update, # Review collapse + rebuttal expand buttons
1438
  *rebuttal_updates, # 10 per-review rebuttals
1439
  general_rebuttal_update, # General rebuttal section
1440
  state
 
1478
  label="Most Divergent Opinions",
1479
  )
1480
 
1481
+ # Topic color legend (HTML version)
1482
+ topic_text_box = gr.HTML(visible=False, value="")
 
 
 
 
 
1483
 
1484
  with gr.Row():
1485
  gr.Markdown("### ๐Ÿ“ Reviews", elem_classes=["review-section-header"])
1486
+ prep_toggle_bar = gr.HTML(visible=False, value="")
 
 
 
1487
  review1 = gr.HighlightedText(show_legend=False, label="๐Ÿ“ Review 1", visible=number_of_displayed_reviews >= 1, key="initial_review1")
1488
  prep_agreement1 = gr.HTML(visible=False, value="")
1489
  prep_rebuttal1 = gr.HTML(visible=False, value="")
 
1539
  return update_review_display(state, score_type)
1540
 
1541
  # Hook up the callbacks with the session state.
1542
+ _review_outputs = [review_id, review1, review2, review3, review4, review5, review6, review7, review8, review9, review10, prep_agreement1, prep_agreement2, prep_agreement3, prep_agreement4, prep_agreement5, prep_agreement6, prep_agreement7, prep_agreement8, prep_agreement9, prep_agreement10, most_common_sentences, most_unique_sentences, topic_text_box, prep_toggle_bar, prep_rebuttal1, prep_rebuttal2, prep_rebuttal3, prep_rebuttal4, prep_rebuttal5, prep_rebuttal6, prep_rebuttal7, prep_rebuttal8, prep_rebuttal9, prep_rebuttal10, prep_general_rebuttal, state]
1543
  year.change(fn=year_change, inputs=[year, state, score_type], outputs=_review_outputs)
1544
  score_type.change(fn=update_review_display, inputs=[state, score_type], outputs=_review_outputs)
1545
  next_button.click(fn=next_review, inputs=[state, score_type], outputs=_review_outputs)
 
1699
  general_formatted = format_general_rebuttals(rebuttal or "")
1700
  has_general = bool(general_formatted)
1701
 
1702
+ # Toggle bar: review collapse + rebuttal expand
1703
  has_any = any(bool(format_rebuttal_for_review(rebuttal or "", i)) for i in range(1, 7)) or has_general
1704
+ toggle_buttons = [_review_toggle_html()]
1705
  if has_any:
1706
+ toggle_buttons.append(_rebuttal_toggle_html())
1707
+ toggle_bar = (
1708
+ '<div style="display:flex;justify-content:flex-end;gap:8px;">'
1709
+ + "".join(toggle_buttons) + '</div>'
1710
+ )
1711
+ toggle_update = gr.update(visible=True, value=toggle_bar)
1712
 
1713
  return (
1714
  gr.update(visible=False), # input_section