Maharshi Gor commited on
Commit
3b39b49
·
1 Parent(s): 22e8b31

Updates and Refactor in QB Interfaces:

Browse files

* Tooltips for model buzz, card style for questions
* Remove reset button for temperature
* Remove unused logging

app.py CHANGED
@@ -6,7 +6,7 @@ from huggingface_hub import snapshot_download
6
  from app_configs import AVAILABLE_MODELS, DEFAULT_SELECTIONS, THEME
7
  from components.quizbowl.bonus import BonusInterface
8
  from components.quizbowl.tossup import TossupInterface
9
- from display.custom_css import css_pipeline, css_tossup
10
  from display.guide import GUIDE_MARKDOWN
11
 
12
  # Constants
@@ -148,7 +148,7 @@ if __name__ == "__main__":
148
  scheduler.add_job(restart_space, "interval", seconds=1800)
149
  scheduler.start()
150
 
151
- full_css = css_pipeline + css_tossup
152
  tossup_ds = load_dataset("tossup")
153
  bonus_ds = load_dataset("bonus")
154
  with gr.Blocks(
 
6
  from app_configs import AVAILABLE_MODELS, DEFAULT_SELECTIONS, THEME
7
  from components.quizbowl.bonus import BonusInterface
8
  from components.quizbowl.tossup import TossupInterface
9
+ from display.custom_css import css_bonus, css_pipeline, css_tossup
10
  from display.guide import GUIDE_MARKDOWN
11
 
12
  # Constants
 
148
  scheduler.add_job(restart_space, "interval", seconds=1800)
149
  scheduler.start()
150
 
151
+ full_css = css_pipeline + css_tossup + css_bonus
152
  tossup_ds = load_dataset("tossup")
153
  bonus_ds = load_dataset("bonus")
154
  with gr.Blocks(
src/components/model_pipeline/state_manager.py CHANGED
@@ -1,5 +1,4 @@
1
  import json
2
- import logging
3
  from typing import Any, Literal
4
 
5
  import gradio as gr
@@ -31,7 +30,6 @@ class ModelStepUIState(BaseModel):
31
  def update(self, key: str, value: Any) -> "ModelStepUIState":
32
  """Update the UI state."""
33
  new_state = self.model_copy(update={key: value})
34
- logging.warning("UI state updated: %s", self)
35
  return new_state
36
 
37
 
 
1
  import json
 
2
  from typing import Any, Literal
3
 
4
  import gradio as gr
 
30
  def update(self, key: str, value: Any) -> "ModelStepUIState":
31
  """Update the UI state."""
32
  new_state = self.model_copy(update={key: value})
 
33
  return new_state
34
 
35
 
src/components/model_step/model_step.py CHANGED
@@ -244,6 +244,7 @@ class ModelStepComponent(FormComponent):
244
  step=0.05,
245
  info="Temperature",
246
  show_label=False,
 
247
  )
248
 
249
  def render(self):
 
244
  step=0.05,
245
  info="Temperature",
246
  show_label=False,
247
+ show_reset_button=False,
248
  )
249
 
250
  def render(self):
src/components/quizbowl/bonus.py CHANGED
@@ -3,8 +3,6 @@ import logging
3
  from typing import Any
4
 
5
  import gradio as gr
6
- import matplotlib.pyplot as plt
7
- import numpy as np
8
  import pandas as pd
9
  from datasets import Dataset
10
 
@@ -14,17 +12,14 @@ from workflows.qb.multi_step_agent import MultiStepBonusAgent
14
  from workflows.qb.simple_agent import SimpleBonusAgent
15
  from workflows.structs import ModelStep, Workflow
16
 
 
17
  from .plotting import (
18
- create_pyplot,
 
19
  create_scatter_pyplot,
20
- evaluate_buzz,
21
- update_plot,
22
  )
23
-
24
-
25
- def evaluate_bonus_part(prediction: str, clean_answers: list[str]) -> float:
26
- """Evaluate a single bonus part."""
27
- return evaluate_buzz(prediction, clean_answers)
28
 
29
 
30
  def process_bonus_results(results: list[dict]) -> pd.DataFrame:
@@ -46,13 +41,7 @@ def process_bonus_results(results: list[dict]) -> pd.DataFrame:
46
  def initialize_eval_interface(example: dict, model_outputs: list[dict]):
47
  """Initialize the interface with example text."""
48
  try:
49
- # Create HTML for leadin and parts
50
- leadin_html = f"<div class='leadin'>{example['leadin']}</div>"
51
- parts_html = []
52
- for i, part in enumerate(example["parts"]):
53
- parts_html.append(f"<div class='part'><b>Part {i + 1}:</b> {part['part']}</div>")
54
-
55
- html_content = f"{leadin_html}<div class='parts-container'>{''.join(parts_html)}</div>"
56
 
57
  # Create confidence plot data
58
  plot_data = create_bonus_confidence_plot(example["parts"], model_outputs)
@@ -66,33 +55,6 @@ def initialize_eval_interface(example: dict, model_outputs: list[dict]):
66
  return f"<div>Error initializing interface: {str(e)}</div>", pd.DataFrame(), "{}"
67
 
68
 
69
- def create_bonus_confidence_plot(parts: list[dict], model_outputs: list[dict]):
70
- """Create confidence plot for bonus parts."""
71
- plt.style.use("ggplot")
72
- fig = plt.figure(figsize=(10, 6))
73
- ax = fig.add_subplot(111)
74
-
75
- # Plot confidence for each part
76
- x = range(1, len(parts) + 1)
77
- confidences = [output["confidence"] for output in model_outputs]
78
- scores = [output["score"] for output in model_outputs]
79
-
80
- # Plot confidence bars
81
- bars = ax.bar(x, confidences, color="#4698cf")
82
-
83
- # Color bars based on correctness
84
- for i, score in enumerate(scores):
85
- bars[i].set_color("green" if score == 1 else "red")
86
-
87
- ax.set_title("Part Confidence")
88
- ax.set_xlabel("Part Number")
89
- ax.set_ylabel("Confidence")
90
- ax.set_xticks(x)
91
- ax.set_xticklabels([f"Part {i}" for i in x])
92
-
93
- return fig
94
-
95
-
96
  def validate_workflow(workflow: Workflow):
97
  """Validate that a workflow is properly configured for the bonus task."""
98
  if not workflow.steps:
@@ -165,27 +127,14 @@ class BonusInterface:
165
  simple=simple,
166
  model_options=list(self.model_options.keys()),
167
  )
168
- with gr.Row():
169
- self.run_btn = gr.Button("Run Bonus", variant="primary")
170
 
171
  def _render_qb_interface(self):
172
  """Render the quizbowl interface."""
173
- with gr.Row():
174
- self.qid_selector = gr.Number(
175
- label="Question ID", value=1, precision=0, minimum=1, maximum=len(self.ds), show_label=True, scale=0
176
- )
177
- self.answer_display = gr.Textbox(
178
- label="Answers", elem_id="answer-display", elem_classes="answer-box", interactive=False, scale=1
179
- )
180
- self.clean_answer_display = gr.Textbox(
181
- label="Acceptable Answers",
182
- elem_id="answer-display-2",
183
- elem_classes="answer-box",
184
- interactive=False,
185
- scale=2,
186
- )
187
 
188
- self.question_display = gr.HTML(label="Question", elem_id="question-display")
189
  with gr.Row():
190
  self.confidence_plot = gr.Plot(
191
  label="Part Confidence",
@@ -198,7 +147,7 @@ class BonusInterface:
198
  )
199
 
200
  with gr.Row():
201
- self.eval_btn = gr.Button("Evaluate")
202
 
203
  with gr.Accordion("Model Submission", elem_classes="model-submission-accordion", open=True):
204
  with gr.Row():
@@ -206,7 +155,7 @@ class BonusInterface:
206
  self.description_input = gr.Textbox(label="Description")
207
  with gr.Row():
208
  gr.LoginButton()
209
- self.submit_btn = gr.Button("Submit")
210
  self.submit_status = gr.HTML(label="Submission Status")
211
 
212
  def render(self):
@@ -226,30 +175,20 @@ class BonusInterface:
226
 
227
  def get_new_question_html(self, question_id: int):
228
  """Get the HTML for a new question."""
229
- example = self.ds[question_id - 1]
230
- leadin = example["leadin"]
231
- parts = example["parts"]
232
-
233
- # Create HTML for leadin and parts
234
- leadin_html = f"<div class='leadin'>{leadin}</div>"
235
- parts_html = []
236
- for i, part in enumerate(parts):
237
- parts_html.append(f"<div class='part'>{part['part']}</div>")
238
-
239
- parts_html_str = "<br>".join(parts_html)
240
-
241
- html_content = (
242
- f"<div class='token-container'>{leadin_html}<div class='parts-container'><br>{parts_html_str}</div></div>"
243
- )
244
-
245
- # Format answers
246
- primary_answers = [f"{i + 1}. {part['answer_primary']}" for i, part in enumerate(parts)]
247
- clean_answers = []
248
- for i, part in enumerate(parts):
249
- part_answers = [a for a in part["clean_answers"] if len(a.split()) <= 6]
250
- clean_answers.append(f"{i + 1}. {', '.join(part_answers)}")
251
 
252
- return html_content, "\n".join(primary_answers), "\n".join(clean_answers)
 
 
 
 
 
253
 
254
  def get_model_outputs(self, example: dict, pipeline_state: PipelineState):
255
  """Get the model outputs for a given question ID."""
@@ -267,13 +206,13 @@ class BonusInterface:
267
 
268
  # Add part number and evaluate score
269
  part_output["part_number"] = i + 1
270
- part_output["score"] = evaluate_bonus_part(part_output["answer"], part["clean_answers"])
271
 
272
  outputs.append(part_output)
273
 
274
  return outputs
275
 
276
- def run_bonus(
277
  self,
278
  question_id: int,
279
  pipeline_state: PipelineState,
@@ -302,9 +241,9 @@ class BonusInterface:
302
  import traceback
303
 
304
  error_msg = f"Error: {str(e)}\n{traceback.format_exc()}"
305
- return error_msg, None, None
306
 
307
- def evaluate_bonus(self, pipeline_state: PipelineState, progress: gr.Progress = gr.Progress()):
308
  """Evaluate the bonus questions."""
309
  try:
310
  # Validate inputs
@@ -361,14 +300,15 @@ class BonusInterface:
361
  triggers=[self.app.load, self.qid_selector.change],
362
  fn=self.get_new_question_html,
363
  inputs=[self.qid_selector],
364
- outputs=[self.question_display, self.answer_display, self.clean_answer_display],
365
  )
 
366
  self.run_btn.click(
367
  self.pipeline_interface.validate_workflow,
368
  inputs=[self.pipeline_interface.pipeline_state],
369
  outputs=[self.pipeline_interface.pipeline_state],
370
  ).success(
371
- self.run_bonus,
372
  inputs=[
373
  self.qid_selector,
374
  self.pipeline_interface.pipeline_state,
@@ -382,7 +322,7 @@ class BonusInterface:
382
  )
383
 
384
  self.eval_btn.click(
385
- fn=self.evaluate_bonus,
386
  inputs=[self.pipeline_interface.pipeline_state],
387
  outputs=[self.results_table, self.confidence_plot],
388
  )
@@ -397,7 +337,7 @@ class BonusInterface:
397
  outputs=[self.submit_status],
398
  )
399
  self.hidden_input.change(
400
- fn=update_plot,
401
  inputs=[self.hidden_input, self.output_state],
402
  outputs=[self.confidence_plot],
403
  )
 
3
  from typing import Any
4
 
5
  import gradio as gr
 
 
6
  import pandas as pd
7
  from datasets import Dataset
8
 
 
12
  from workflows.qb.simple_agent import SimpleBonusAgent
13
  from workflows.structs import ModelStep, Workflow
14
 
15
+ from .commons import get_qid_selector
16
  from .plotting import (
17
+ create_bonus_confidence_plot,
18
+ create_bonus_html,
19
  create_scatter_pyplot,
20
+ update_tossup_plot,
 
21
  )
22
+ from .utils import evaluate_prediction
 
 
 
 
23
 
24
 
25
  def process_bonus_results(results: list[dict]) -> pd.DataFrame:
 
41
  def initialize_eval_interface(example: dict, model_outputs: list[dict]):
42
  """Initialize the interface with example text."""
43
  try:
44
+ html_content = create_bonus_html(example["leadin"], example["parts"])
 
 
 
 
 
 
45
 
46
  # Create confidence plot data
47
  plot_data = create_bonus_confidence_plot(example["parts"], model_outputs)
 
55
  return f"<div>Error initializing interface: {str(e)}</div>", pd.DataFrame(), "{}"
56
 
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  def validate_workflow(workflow: Workflow):
59
  """Validate that a workflow is properly configured for the bonus task."""
60
  if not workflow.steps:
 
127
  simple=simple,
128
  model_options=list(self.model_options.keys()),
129
  )
 
 
130
 
131
  def _render_qb_interface(self):
132
  """Render the quizbowl interface."""
133
+ with gr.Row(elem_classes="bonus-header-row form-inline"):
134
+ self.qid_selector = get_qid_selector(len(self.ds))
135
+ self.run_btn = gr.Button("Run on Bonus Question", variant="secondary")
 
 
 
 
 
 
 
 
 
 
 
136
 
137
+ self.question_display = gr.HTML(label="Question", elem_id="bonus-question-display")
138
  with gr.Row():
139
  self.confidence_plot = gr.Plot(
140
  label="Part Confidence",
 
147
  )
148
 
149
  with gr.Row():
150
+ self.eval_btn = gr.Button("Evaluate", variant="primary")
151
 
152
  with gr.Accordion("Model Submission", elem_classes="model-submission-accordion", open=True):
153
  with gr.Row():
 
155
  self.description_input = gr.Textbox(label="Description")
156
  with gr.Row():
157
  gr.LoginButton()
158
+ self.submit_btn = gr.Button("Submit", variant="primary")
159
  self.submit_status = gr.HTML(label="Submission Status")
160
 
161
  def render(self):
 
175
 
176
  def get_new_question_html(self, question_id: int):
177
  """Get the HTML for a new question."""
178
+ if question_id is None:
179
+ logging.error("Question ID is None. Setting to 1")
180
+ question_id = 1
181
+ try:
182
+ question_id = int(question_id) - 1
183
+ if not self.ds or question_id < 0 or question_id >= len(self.ds):
184
+ return "Invalid question ID or dataset not loaded"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
 
186
+ example = self.ds[question_id]
187
+ leadin = example["leadin"]
188
+ parts = example["parts"]
189
+ return create_bonus_html(leadin, parts)
190
+ except Exception as e:
191
+ return f"Error loading question: {str(e)}"
192
 
193
  def get_model_outputs(self, example: dict, pipeline_state: PipelineState):
194
  """Get the model outputs for a given question ID."""
 
206
 
207
  # Add part number and evaluate score
208
  part_output["part_number"] = i + 1
209
+ part_output["score"] = evaluate_prediction(part_output["answer"], part["clean_answers"])
210
 
211
  outputs.append(part_output)
212
 
213
  return outputs
214
 
215
+ def single_run(
216
  self,
217
  question_id: int,
218
  pipeline_state: PipelineState,
 
241
  import traceback
242
 
243
  error_msg = f"Error: {str(e)}\n{traceback.format_exc()}"
244
+ return error_msg, gr.skip(), gr.skip(), gr.skip()
245
 
246
+ def evaluate(self, pipeline_state: PipelineState, progress: gr.Progress = gr.Progress()):
247
  """Evaluate the bonus questions."""
248
  try:
249
  # Validate inputs
 
300
  triggers=[self.app.load, self.qid_selector.change],
301
  fn=self.get_new_question_html,
302
  inputs=[self.qid_selector],
303
+ outputs=[self.question_display],
304
  )
305
+
306
  self.run_btn.click(
307
  self.pipeline_interface.validate_workflow,
308
  inputs=[self.pipeline_interface.pipeline_state],
309
  outputs=[self.pipeline_interface.pipeline_state],
310
  ).success(
311
+ self.single_run,
312
  inputs=[
313
  self.qid_selector,
314
  self.pipeline_interface.pipeline_state,
 
322
  )
323
 
324
  self.eval_btn.click(
325
+ fn=self.evaluate,
326
  inputs=[self.pipeline_interface.pipeline_state],
327
  outputs=[self.results_table, self.confidence_plot],
328
  )
 
337
  outputs=[self.submit_status],
338
  )
339
  self.hidden_input.change(
340
+ fn=update_tossup_plot,
341
  inputs=[self.hidden_input, self.output_state],
342
  outputs=[self.confidence_plot],
343
  )
src/components/quizbowl/commons.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+
3
+
4
+ def get_qid_selector(dataset_size: int):
5
+ return gr.Number(
6
+ info="Question ID",
7
+ value=1,
8
+ precision=0,
9
+ minimum=1,
10
+ maximum=dataset_size,
11
+ show_label=False,
12
+ scale=0,
13
+ container=False,
14
+ elem_classes="qid-selector",
15
+ )
src/components/quizbowl/plotting.py CHANGED
@@ -7,67 +7,145 @@ import matplotlib.pyplot as plt
7
  import pandas as pd
8
 
9
 
10
- def evaluate_buzz(prediction: str, clean_answers: list[str] | str) -> int:
11
- """Evaluate the buzz of a prediction against the clean answers."""
12
- if isinstance(clean_answers, str):
13
- print("clean_answers is a string")
14
- clean_answers = [clean_answers]
15
- pred = prediction.lower().strip()
16
- if not pred:
17
- return 0
18
- for answer in clean_answers:
19
- answer = answer.strip().lower()
20
- if answer and answer in pred:
21
- print(f"Found {answer} in {pred}")
22
- return 1
23
- return 0
24
-
25
-
26
- def create_answer_html(answer: str):
27
- """Create HTML for the answer."""
28
- return f"<div class='answer-header'>Answer:<br>{answer}</div>"
29
-
30
-
31
- def create_tokens_html(tokens: list[str], eval_points: list[tuple], answer: str, marker_indices: list[int] = None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  """Create HTML for tokens with hover capability and a colored header for the answer."""
33
  try:
34
- html_parts = []
35
  ep = dict(eval_points)
36
- marker_indices = set(marker_indices) if isinstance(marker_indices, list) else set()
37
-
38
- # Add a colored header for the answer
39
- # html_parts.append(create_answer_html(answer))
40
 
 
41
  for i, token in enumerate(tokens):
42
- # Check if this token is a buzz point
43
- values = ep.get(i, (None, 0, 0))
44
- confidence, buzz_point, score = values
45
-
46
- # Replace non-word characters for proper display in HTML
47
- display_token = token
48
- if not re.match(r"\w+", token):
49
- display_token = token.replace(" ", "&nbsp;")
50
-
51
- # Add buzz marker class if it's a buzz point
52
- if confidence is None:
53
- css_class = ""
54
- elif not buzz_point:
55
- css_class = " guess-point no-buzz"
56
- else:
57
- css_class = f" guess-point buzz-{score}"
58
-
59
- token_html = f'<span id="token-{i}" class="token{css_class}" data-index="{i}">{display_token}</span>'
60
- if i in marker_indices:
61
- token_html += "<span style='color: rgba(0,0,255,0.3);'>|</span>"
62
- html_parts.append(token_html)
63
-
64
- return f"<div class='token-container'>{''.join(html_parts)}</div>"
65
  except Exception as e:
66
  logging.error(f"Error creating token HTML: {e}", exc_info=True)
67
  return f"<div class='token-container'>Error creating tokens: {str(e)}</div>"
68
 
69
 
70
- def create_line_plot(eval_points, highlighted_index=-1):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  """Create a Gradio LinePlot of token values with optional highlighting using DataFrame."""
72
  try:
73
  # Create base confidence data
@@ -114,26 +192,29 @@ def create_line_plot(eval_points, highlighted_index=-1):
114
  return pd.DataFrame(columns=["position", "value", "type", "highlight", "color"])
115
 
116
 
117
- def create_pyplot(tokens, eval_points, highlighted_index=-1):
 
 
118
  """Create a pyplot of token values with optional highlighting."""
119
  plt.style.use("ggplot") # Set theme to grid paper
120
- fig = plt.figure(figsize=(10, 6)) # Set figure size
121
  ax = fig.add_subplot(111)
122
  x = [0]
123
  y = [0]
124
- for i, (v, b, s) in eval_points:
125
  x.append(i + 1)
126
- y.append(v)
127
 
128
  ax.plot(x, y, "o--", color="#4698cf")
129
- for i, (v, b, s) in eval_points:
130
- if not b:
131
  continue
132
- color = "green" if s else "red"
133
- ax.plot(i + 1, v, "o", color=color)
 
134
  if i >= len(tokens):
135
  print(f"Token index {i} is out of bounds for n_tokens: {len(tokens)}")
136
- ax.annotate(f"{tokens[i]}", (i + 1, v), textcoords="offset points", xytext=(0, 10), ha="center")
137
 
138
  if highlighted_index >= 0:
139
  # Add light vertical line for the highlighted token from 0 to 1
@@ -147,10 +228,10 @@ def create_pyplot(tokens, eval_points, highlighted_index=-1):
147
  return fig
148
 
149
 
150
- def create_scatter_pyplot(token_positions, scores):
151
  """Create a scatter plot of token positions and scores."""
152
  plt.style.use("ggplot")
153
- fig = plt.figure(figsize=(10, 6))
154
  ax = fig.add_subplot(111)
155
 
156
  counts = Counter(zip(token_positions, scores))
@@ -167,7 +248,34 @@ def create_scatter_pyplot(token_positions, scores):
167
  return fig
168
 
169
 
170
- def update_plot(highlighted_index, state):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  """Update the plot when a token is hovered; add a vertical line on the plot."""
172
  try:
173
  if not state or state == "{}":
@@ -187,7 +295,7 @@ def update_plot(highlighted_index, state):
187
 
188
  # Create updated plot with highlighting of the token point
189
  # plot_data = create_line_plot(values, highlighted_index)
190
- plot_data = create_pyplot(tokens, values, highlighted_index)
191
  return plot_data
192
  except Exception as e:
193
  logging.error(f"Error updating plot: {e}")
 
7
  import pandas as pd
8
 
9
 
10
+ def _make_answer_html(answer: str, clean_answers: list[str] = []) -> str:
11
+ clean_answers = [a for a in clean_answers if len(a.split()) <= 6 and a != answer]
12
+ additional_answers_html = ""
13
+ if clean_answers:
14
+ additional_answers_html = f"<span class='bonus-answer-text'> [or {', '.join(clean_answers)}]</span>"
15
+ return f"""
16
+ <div class='bonus-answer'>
17
+ <span class='bonus-answer-label'>Answer: </span>
18
+ <span class='bonus-answer-text'>{answer}</span>
19
+ {additional_answers_html}
20
+ </div>
21
+ """
22
+
23
+
24
+ def _get_token_classes(confidence, buzz, score) -> str:
25
+ if confidence is None:
26
+ return "token"
27
+ elif not buzz:
28
+ return "token guess-point no-buzz"
29
+ else:
30
+ return f"token guess-point buzz-{score}"
31
+
32
+
33
+ def _create_token_tooltip_html(values) -> str:
34
+ if not values:
35
+ return ""
36
+ confidence = values.get("confidence", 0)
37
+ buzz = values.get("buzz", 0)
38
+ score = values.get("score", 0)
39
+ answer = values.get("answer", "")
40
+ answer_tokens = answer.split()
41
+ if len(answer_tokens) > 10:
42
+ k = len(answer_tokens) - 10
43
+ answer = " ".join(answer_tokens[:10]) + f"...[{k} more words]"
44
+
45
+ color = "#a3c9a3" if score else "#ebbec4" # Light green for correct, light pink for incorrect
46
+
47
+ return f"""
48
+ <div class="tooltip card" style="background-color: {color}; border-radius: 8px; padding: 12px; box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.15);">
49
+ <div class="tooltip-content" style="font-family: 'Arial', sans-serif; color: #333;">
50
+ <h4 style="margin: 0 0 8px;">💡 Answer</h4>
51
+ <p style="font-weight: bold; margin: 0 0 8px;">{answer}</p>
52
+ <p style="margin: 0 0 4px;">📊 <strong>Confidence:</strong> {confidence:.2f}</p>
53
+ <p style="margin: 0;">🔍 <strong>Status:</strong> {"✅ Correct" if score else "❌ Incorrect" if buzz else "🚫 No Buzz"}</p>
54
+ </div>
55
+ </div>
56
+ """
57
+
58
+
59
+ def create_token_html(token: str, values: dict, i: int) -> str:
60
+ confidence = values.get("confidence", None)
61
+ buzz = values.get("buzz", 0)
62
+ score = values.get("score", 0)
63
+
64
+ # Replace non-word characters for proper display in HTML
65
+ display_token = f"{token} 🚨" if buzz else f"{token} 💭" if values else token
66
+ if not re.match(r"\w+", token):
67
+ display_token = token.replace(" ", "&nbsp;")
68
+
69
+ css_class = _get_token_classes(confidence, buzz, score)
70
+ # Add tooltip if we have values for this token
71
+ tooltip_html = _create_token_tooltip_html(values)
72
+
73
+ token_html = f'<span id="token-{i}" class="{css_class}" data-index="{i}">{display_token}{tooltip_html}</span>'
74
+ # if i in marker_indices:
75
+ # token_html += "<span style='color: crimson;'>|</span>"
76
+ return token_html
77
+
78
+
79
+ def create_tossup_html(
80
+ tokens: list[str],
81
+ answer_primary: str,
82
+ clean_answers: list[str],
83
+ marker_indices: list[int] = [],
84
+ eval_points: list[tuple[int, dict]] = [],
85
+ ) -> str:
86
  """Create HTML for tokens with hover capability and a colored header for the answer."""
87
  try:
 
88
  ep = dict(eval_points)
89
+ marker_indices = set(marker_indices)
 
 
 
90
 
91
+ html_tokens = []
92
  for i, token in enumerate(tokens):
93
+ token_html = create_token_html(token, ep.get(i, {}), i + 1)
94
+ html_tokens.append(token_html)
95
+
96
+ answer_html = _make_answer_html(answer_primary, clean_answers)
97
+ return f"""
98
+ <div class='bonus-container'>
99
+ <div class='bonus-card'>
100
+ <div class='tossup-question'>
101
+ {"".join(html_tokens)}
102
+ </div>
103
+ {answer_html}
104
+ </div>
105
+ </div>
106
+ """
 
 
 
 
 
 
 
 
 
107
  except Exception as e:
108
  logging.error(f"Error creating token HTML: {e}", exc_info=True)
109
  return f"<div class='token-container'>Error creating tokens: {str(e)}</div>"
110
 
111
 
112
+ def create_bonus_html(leadin: str, parts: list[dict]) -> str:
113
+ # Create HTML for leadin and parts with answers
114
+ leadin_html = f"<div class='bonus-leadin'>{leadin}</div>"
115
+ parts_html = []
116
+
117
+ for i, part in enumerate(parts):
118
+ question_text = part["part"]
119
+ answer_html = _make_answer_html(part["answer_primary"], part["clean_answers"])
120
+
121
+ "<div class='bonus-part-number'>Part {i + 1}</div>"
122
+ part_html = f"""
123
+ <div class='bonus-part'>
124
+ <div class='bonus-part-text'><b>#{i + 1}.</b> {question_text}</div>
125
+ {answer_html}
126
+ </div>
127
+ """
128
+ parts_html.append(part_html)
129
+
130
+ html_content = f"""
131
+ <div class='bonus-container'>
132
+ <div class='bonus-card'>
133
+ {leadin_html}
134
+ {"".join(parts_html)}
135
+ </div>
136
+ </div>
137
+ """
138
+
139
+ # Format clean answers for the answer display
140
+ clean_answers = []
141
+ for i, part in enumerate(parts):
142
+ part_answers = [a for a in part["clean_answers"] if len(a.split()) <= 6]
143
+ clean_answers.append(f"{i + 1}. {', '.join(part_answers)}")
144
+
145
+ return html_content
146
+
147
+
148
+ def create_line_plot(eval_points: list[tuple[int, dict]], highlighted_index: int = -1) -> pd.DataFrame:
149
  """Create a Gradio LinePlot of token values with optional highlighting using DataFrame."""
150
  try:
151
  # Create base confidence data
 
192
  return pd.DataFrame(columns=["position", "value", "type", "highlight", "color"])
193
 
194
 
195
+ def create_tossup_confidence_pyplot(
196
+ tokens: list[str], eval_points: list[tuple[int, dict]], highlighted_index: int = -1
197
+ ) -> plt.Figure:
198
  """Create a pyplot of token values with optional highlighting."""
199
  plt.style.use("ggplot") # Set theme to grid paper
200
+ fig = plt.figure(figsize=(11, 5)) # Set figure size to 11x5
201
  ax = fig.add_subplot(111)
202
  x = [0]
203
  y = [0]
204
+ for i, v in eval_points:
205
  x.append(i + 1)
206
+ y.append(v["confidence"])
207
 
208
  ax.plot(x, y, "o--", color="#4698cf")
209
+ for i, v in eval_points:
210
+ if not v["buzz"]:
211
  continue
212
+ confidence = v["confidence"]
213
+ color = "green" if v["score"] else "red"
214
+ ax.plot(i + 1, confidence, "o", color=color)
215
  if i >= len(tokens):
216
  print(f"Token index {i} is out of bounds for n_tokens: {len(tokens)}")
217
+ ax.annotate(f"{tokens[i]}", (i + 1, confidence), textcoords="offset points", xytext=(0, 10), ha="center")
218
 
219
  if highlighted_index >= 0:
220
  # Add light vertical line for the highlighted token from 0 to 1
 
228
  return fig
229
 
230
 
231
+ def create_scatter_pyplot(token_positions: list[int], scores: list[int]) -> plt.Figure:
232
  """Create a scatter plot of token positions and scores."""
233
  plt.style.use("ggplot")
234
+ fig = plt.figure(figsize=(11, 5))
235
  ax = fig.add_subplot(111)
236
 
237
  counts = Counter(zip(token_positions, scores))
 
248
  return fig
249
 
250
 
251
+ def create_bonus_confidence_plot(parts: list[dict], model_outputs: list[dict]) -> plt.Figure:
252
+ """Create confidence plot for bonus parts."""
253
+ plt.style.use("ggplot")
254
+ fig = plt.figure(figsize=(10, 6))
255
+ ax = fig.add_subplot(111)
256
+
257
+ # Plot confidence for each part
258
+ x = range(1, len(parts) + 1)
259
+ confidences = [output["confidence"] for output in model_outputs]
260
+ scores = [output["score"] for output in model_outputs]
261
+
262
+ # Plot confidence bars
263
+ bars = ax.bar(x, confidences, color="#4698cf")
264
+
265
+ # Color bars based on correctness
266
+ for i, score in enumerate(scores):
267
+ bars[i].set_color("green" if score == 1 else "red")
268
+
269
+ ax.set_title("Part Confidence")
270
+ ax.set_xlabel("Part Number")
271
+ ax.set_ylabel("Confidence")
272
+ ax.set_xticks(x)
273
+ ax.set_xticklabels([f"Part {i}" for i in x])
274
+
275
+ return fig
276
+
277
+
278
+ def update_tossup_plot(highlighted_index: int, state: str) -> pd.DataFrame:
279
  """Update the plot when a token is hovered; add a vertical line on the plot."""
280
  try:
281
  if not state or state == "{}":
 
295
 
296
  # Create updated plot with highlighting of the token point
297
  # plot_data = create_line_plot(values, highlighted_index)
298
+ plot_data = create_tossup_confidence_pyplot(tokens, values, highlighted_index)
299
  return plot_data
300
  except Exception as e:
301
  logging.error(f"Error updating plot: {e}")
src/components/quizbowl/tossup.py CHANGED
@@ -13,14 +13,14 @@ from workflows.qb.multi_step_agent import MultiStepTossupAgent
13
  from workflows.qb.simple_agent import SimpleTossupAgent
14
  from workflows.structs import ModelStep, Workflow
15
 
 
16
  from .plotting import (
17
- create_answer_html,
18
- create_pyplot,
19
  create_scatter_pyplot,
20
- create_tokens_html,
21
- evaluate_buzz,
22
- update_plot,
23
  )
 
24
 
25
  # TODO: Error handling on run tossup and evaluate tossup and show correct messages
26
  # TODO: ^^ Same for Bonus
@@ -29,7 +29,7 @@ from .plotting import (
29
  def add_model_scores(model_outputs: list[dict], clean_answers: list[str], run_indices: list[int]) -> list[dict]:
30
  """Add model scores to the model outputs."""
31
  for output, run_idx in zip(model_outputs, run_indices):
32
- output["score"] = evaluate_buzz(output["answer"], clean_answers)
33
  output["token_position"] = run_idx + 1
34
  return model_outputs
35
 
@@ -43,26 +43,25 @@ def prepare_buzz_evals(
43
  return [], []
44
  eval_points = []
45
  for i, v in zip(run_indices, model_outputs):
46
- eval_point = v["confidence"], v["buzz"], v["score"]
47
- eval_points.append((int(i), eval_point))
48
 
49
  return eval_points
50
 
51
 
52
  def initialize_eval_interface(example, model_outputs: list[dict]):
53
  """Initialize the interface with example text."""
54
- tokens = example["question"].split()
55
- run_indices = example["run_indices"]
56
- answer = example["answer_primary"]
57
-
58
  try:
 
 
 
 
59
  eval_points = prepare_buzz_evals(run_indices, model_outputs)
60
 
61
  if not tokens:
62
  return "<div>No tokens found in the provided text.</div>", pd.DataFrame(), "{}"
63
- highlighted_index = next((int(i) for i, (_, b, _) in eval_points if b == 1), -1)
64
- html_content = create_tokens_html(tokens, eval_points, answer)
65
- plot_data = create_pyplot(tokens, eval_points, highlighted_index)
66
 
67
  # Store tokens, values, and buzzes as JSON for later use
68
  state = json.dumps({"tokens": tokens, "values": eval_points})
@@ -195,26 +194,13 @@ class TossupInterface:
195
  label="Early Stop",
196
  info="Stop early if already buzzed",
197
  )
198
- self.run_btn = gr.Button("Run Tossup", variant="primary")
199
 
200
  def _render_qb_interface(self):
201
  """Render the quizbowl interface."""
202
- with gr.Row():
203
- self.qid_selector = gr.Number(
204
- label="Question ID", value=1, precision=0, minimum=1, maximum=len(self.ds), show_label=True, scale=0
205
- )
206
- self.answer_display = gr.Textbox(
207
- label="PrimaryAnswer", elem_id="answer-display", elem_classes="answer-box", interactive=False, scale=1
208
- )
209
- self.clean_answer_display = gr.Textbox(
210
- label="Acceptable Answers",
211
- elem_id="answer-display-2",
212
- elem_classes="answer-box",
213
- interactive=False,
214
- scale=2,
215
- )
216
- # self.answer_display = gr.HTML(label="Answer", elem_id="answer-display")
217
- self.question_display = gr.HTML(label="Question", elem_id="question-display")
218
  with gr.Row():
219
  self.confidence_plot = gr.Plot(
220
  label="Buzz Confidence",
@@ -225,7 +211,7 @@ class TossupInterface:
225
  value=pd.DataFrame(columns=["Token Position", "Correct?", "Confidence", "Prediction"]),
226
  )
227
  with gr.Row():
228
- self.eval_btn = gr.Button("Evaluate")
229
 
230
  with gr.Accordion("Model Submission", elem_classes="model-submission-accordion", open=True):
231
  with gr.Row():
@@ -233,7 +219,7 @@ class TossupInterface:
233
  self.description_input = gr.Textbox(label="Description")
234
  with gr.Row():
235
  gr.LoginButton()
236
- self.submit_btn = gr.Button("Submit")
237
  self.submit_status = gr.HTML(label="Submission Status")
238
 
239
  def render(self):
@@ -253,22 +239,6 @@ class TossupInterface:
253
 
254
  self._setup_event_listeners()
255
 
256
- def get_full_question(self, question_id: int) -> str:
257
- """Get the full question text for a given question ID."""
258
- try:
259
- question_id = int(question_id - 1)
260
- if not self.ds or question_id < 0 or question_id >= len(self.ds):
261
- return "Invalid question ID or dataset not loaded"
262
-
263
- question_data = self.ds[question_id]
264
- # Get the full question text (the last element in question_runs)
265
- full_question = question_data["question"]
266
- gold_label = question_data["answer_primary"]
267
-
268
- return f"Question: {full_question}\n\nCorrect Answer: {gold_label}"
269
- except Exception as e:
270
- return f"Error loading question: {str(e)}"
271
-
272
  def validate_workflow(self, pipeline_state: PipelineState):
273
  """Validate the workflow."""
274
  try:
@@ -276,17 +246,19 @@ class TossupInterface:
276
  except Exception as e:
277
  raise gr.Error(f"Error validating workflow: {str(e)}")
278
 
279
- def get_new_question_html(self, question_id: int):
280
  """Get the HTML for a new question."""
281
- example = self.ds[question_id - 1]
282
- question = example["question"]
283
- gold_label = example["answer_primary"]
284
- marker_indices = example["run_indices"]
285
- tokens = question.split()
286
- question_html = create_tokens_html(tokens, [], gold_label, marker_indices)
287
- clean_answers = [a for a in example["clean_answers"] if len(a.split()) <= 6]
288
- clean_answers = ", ".join(clean_answers)
289
- return question_html, gold_label, clean_answers
 
 
290
 
291
  def get_model_outputs(self, example: dict, pipeline_state: PipelineState, buzz_threshold: float, early_stop: bool):
292
  """Get the model outputs for a given question ID."""
@@ -304,7 +276,7 @@ class TossupInterface:
304
  outputs = add_model_scores(outputs, example["clean_answers"], example["run_indices"])
305
  return outputs
306
 
307
- def run_tossup(
308
  self,
309
  question_id: int,
310
  pipeline_state: PipelineState,
@@ -335,10 +307,8 @@ class TossupInterface:
335
  error_msg = f"Error: {str(e)}\n{traceback.format_exc()}"
336
  return error_msg, None, None
337
 
338
- def evaluate_tossups(
339
- self, pipeline_state: PipelineState, buzz_threshold: float, progress: gr.Progress = gr.Progress()
340
- ):
341
- """Evaluate the tossup."""
342
  try:
343
  # Validate inputs
344
  if not self.ds or not self.ds.num_rows:
@@ -388,7 +358,7 @@ class TossupInterface:
388
  triggers=[self.app.load, self.qid_selector.change],
389
  fn=self.get_new_question_html,
390
  inputs=[self.qid_selector],
391
- outputs=[self.question_display, self.answer_display, self.clean_answer_display],
392
  )
393
 
394
  self.run_btn.click(
@@ -396,7 +366,7 @@ class TossupInterface:
396
  inputs=[self.pipeline_interface.pipeline_state],
397
  outputs=[self.pipeline_interface.pipeline_state],
398
  ).success(
399
- self.run_tossup,
400
  inputs=[
401
  self.qid_selector,
402
  self.pipeline_interface.pipeline_state,
@@ -412,7 +382,7 @@ class TossupInterface:
412
  )
413
 
414
  self.eval_btn.click(
415
- fn=self.evaluate_tossups,
416
  inputs=[self.pipeline_interface.pipeline_state, self.buzz_t_slider],
417
  outputs=[self.results_table, self.confidence_plot],
418
  )
@@ -428,7 +398,7 @@ class TossupInterface:
428
  )
429
 
430
  self.hidden_input.change(
431
- fn=update_plot,
432
  inputs=[self.hidden_input, self.output_state],
433
  outputs=[self.confidence_plot],
434
  )
 
13
  from workflows.qb.simple_agent import SimpleTossupAgent
14
  from workflows.structs import ModelStep, Workflow
15
 
16
+ from .commons import get_qid_selector
17
  from .plotting import (
 
 
18
  create_scatter_pyplot,
19
+ create_tossup_confidence_pyplot,
20
+ create_tossup_html,
21
+ update_tossup_plot,
22
  )
23
+ from .utils import evaluate_prediction
24
 
25
  # TODO: Error handling on run tossup and evaluate tossup and show correct messages
26
  # TODO: ^^ Same for Bonus
 
29
  def add_model_scores(model_outputs: list[dict], clean_answers: list[str], run_indices: list[int]) -> list[dict]:
30
  """Add model scores to the model outputs."""
31
  for output, run_idx in zip(model_outputs, run_indices):
32
+ output["score"] = evaluate_prediction(output["answer"], clean_answers)
33
  output["token_position"] = run_idx + 1
34
  return model_outputs
35
 
 
43
  return [], []
44
  eval_points = []
45
  for i, v in zip(run_indices, model_outputs):
46
+ eval_points.append((int(i), v))
 
47
 
48
  return eval_points
49
 
50
 
51
  def initialize_eval_interface(example, model_outputs: list[dict]):
52
  """Initialize the interface with example text."""
 
 
 
 
53
  try:
54
+ tokens = example["question"].split()
55
+ run_indices = example["run_indices"]
56
+ answer = example["answer_primary"]
57
+ clean_answers = example["clean_answers"]
58
  eval_points = prepare_buzz_evals(run_indices, model_outputs)
59
 
60
  if not tokens:
61
  return "<div>No tokens found in the provided text.</div>", pd.DataFrame(), "{}"
62
+ highlighted_index = next((int(i) for i, v in eval_points if v["buzz"] == 1), -1)
63
+ html_content = create_tossup_html(tokens, answer, clean_answers, run_indices, eval_points)
64
+ plot_data = create_tossup_confidence_pyplot(tokens, eval_points, highlighted_index)
65
 
66
  # Store tokens, values, and buzzes as JSON for later use
67
  state = json.dumps({"tokens": tokens, "values": eval_points})
 
194
  label="Early Stop",
195
  info="Stop early if already buzzed",
196
  )
 
197
 
198
  def _render_qb_interface(self):
199
  """Render the quizbowl interface."""
200
+ with gr.Row(elem_classes="bonus-header-row form-inline"):
201
+ self.qid_selector = get_qid_selector(len(self.ds))
202
+ self.run_btn = gr.Button("Run on Tossup Question", variant="secondary")
203
+ self.question_display = gr.HTML(label="Question", elem_id="tossup-question-display")
 
 
 
 
 
 
 
 
 
 
 
 
204
  with gr.Row():
205
  self.confidence_plot = gr.Plot(
206
  label="Buzz Confidence",
 
211
  value=pd.DataFrame(columns=["Token Position", "Correct?", "Confidence", "Prediction"]),
212
  )
213
  with gr.Row():
214
+ self.eval_btn = gr.Button("Evaluate", variant="primary")
215
 
216
  with gr.Accordion("Model Submission", elem_classes="model-submission-accordion", open=True):
217
  with gr.Row():
 
219
  self.description_input = gr.Textbox(label="Description")
220
  with gr.Row():
221
  gr.LoginButton()
222
+ self.submit_btn = gr.Button("Submit", variant="primary")
223
  self.submit_status = gr.HTML(label="Submission Status")
224
 
225
  def render(self):
 
239
 
240
  self._setup_event_listeners()
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  def validate_workflow(self, pipeline_state: PipelineState):
243
  """Validate the workflow."""
244
  try:
 
246
  except Exception as e:
247
  raise gr.Error(f"Error validating workflow: {str(e)}")
248
 
249
+ def get_new_question_html(self, question_id: int) -> str:
250
  """Get the HTML for a new question."""
251
+ if question_id is None:
252
+ logging.error("Question ID is None. Setting to 1")
253
+ question_id = 1
254
+ try:
255
+ example = self.ds[question_id - 1]
256
+ question_tokens = example["question"].split()
257
+ return create_tossup_html(
258
+ question_tokens, example["answer_primary"], example["clean_answers"], example["run_indices"]
259
+ )
260
+ except Exception as e:
261
+ return f"Error loading question: {str(e)}"
262
 
263
  def get_model_outputs(self, example: dict, pipeline_state: PipelineState, buzz_threshold: float, early_stop: bool):
264
  """Get the model outputs for a given question ID."""
 
276
  outputs = add_model_scores(outputs, example["clean_answers"], example["run_indices"])
277
  return outputs
278
 
279
+ def single_run(
280
  self,
281
  question_id: int,
282
  pipeline_state: PipelineState,
 
307
  error_msg = f"Error: {str(e)}\n{traceback.format_exc()}"
308
  return error_msg, None, None
309
 
310
+ def evaluate(self, pipeline_state: PipelineState, buzz_threshold: float, progress: gr.Progress = gr.Progress()):
311
+ """Evaluate the tossup questions."""
 
 
312
  try:
313
  # Validate inputs
314
  if not self.ds or not self.ds.num_rows:
 
358
  triggers=[self.app.load, self.qid_selector.change],
359
  fn=self.get_new_question_html,
360
  inputs=[self.qid_selector],
361
+ outputs=[self.question_display],
362
  )
363
 
364
  self.run_btn.click(
 
366
  inputs=[self.pipeline_interface.pipeline_state],
367
  outputs=[self.pipeline_interface.pipeline_state],
368
  ).success(
369
+ self.single_run,
370
  inputs=[
371
  self.qid_selector,
372
  self.pipeline_interface.pipeline_state,
 
382
  )
383
 
384
  self.eval_btn.click(
385
+ fn=self.evaluate,
386
  inputs=[self.pipeline_interface.pipeline_state, self.buzz_t_slider],
387
  outputs=[self.results_table, self.confidence_plot],
388
  )
 
398
  )
399
 
400
  self.hidden_input.change(
401
+ fn=update_tossup_plot,
402
  inputs=[self.hidden_input, self.output_state],
403
  outputs=[self.confidence_plot],
404
  )
src/components/quizbowl/utils.py CHANGED
@@ -3,6 +3,22 @@ from typing import Any, Dict, List
3
  import pandas as pd
4
 
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  def _create_confidence_plot_data(results: List[Dict], top_k_mode: bool = False) -> pd.DataFrame:
7
  """Create a DataFrame for the confidence plot."""
8
  if not top_k_mode:
@@ -59,28 +75,3 @@ def _create_top_k_dataframe(results: List[Dict]) -> pd.DataFrame:
59
  }
60
  )
61
  return pd.DataFrame(df_rows)
62
-
63
-
64
- def _format_buzz_result(buzzed: bool, results: List[Dict], gold_label: str, top_k_mode: bool) -> tuple[str, str, bool]:
65
- """Format the result text based on whether the agent buzzed."""
66
- if not buzzed:
67
- return f"Did not buzz. Correct answer was: {gold_label}", "No buzz", False
68
-
69
- buzz_position = next(i for i, r in enumerate(results) if r.get("buzz", False))
70
- buzz_result = results[buzz_position]
71
-
72
- if top_k_mode:
73
- # For top-k, check if any of the top guesses match
74
- top_answers = [g.get("answer", "").lower() for g in buzz_result.get("guesses", [])]
75
- correct = gold_label.lower() in [a.lower() for a in top_answers]
76
- final_answer = top_answers[0] if top_answers else "No answer"
77
- else:
78
- # For regular mode
79
- final_answer = buzz_result["answer"]
80
- correct = final_answer.lower() == gold_label.lower()
81
-
82
- result_text = f"BUZZED at position {buzz_position + 1} with answer: {final_answer}\n"
83
- result_text += f"Correct answer: {gold_label}\n"
84
- result_text += f"Result: {'CORRECT' if correct else 'INCORRECT'}"
85
-
86
- return result_text, final_answer, correct
 
3
  import pandas as pd
4
 
5
 
6
+ def evaluate_prediction(prediction: str, clean_answers: list[str] | str) -> int:
7
+ """Evaluate the buzz of a prediction against the clean answers."""
8
+ if isinstance(clean_answers, str):
9
+ print("clean_answers is a string")
10
+ clean_answers = [clean_answers]
11
+ pred = prediction.lower().strip()
12
+ if not pred:
13
+ return 0
14
+ for answer in clean_answers:
15
+ answer = answer.strip().lower()
16
+ if answer and answer in pred:
17
+ print(f"Found {answer} in {pred}")
18
+ return 1
19
+ return 0
20
+
21
+
22
  def _create_confidence_plot_data(results: List[Dict], top_k_mode: bool = False) -> pd.DataFrame:
23
  """Create a DataFrame for the confidence plot."""
24
  if not top_k_mode:
 
75
  }
76
  )
77
  return pd.DataFrame(df_rows)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/display/custom_css.py CHANGED
@@ -420,7 +420,7 @@ css_tossup = """
420
  .token.buzz-1 {
421
  border-color: #228b22; /* Darker and slightly muted green */
422
  }
423
- .token-container {
424
  line-height: 1.7;
425
  padding: 5px;
426
  margin-left: 4px;
@@ -429,4 +429,116 @@ css_tossup = """
429
  border-radius: 8px;
430
  margin-bottom: 10px;
431
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  """
 
420
  .token.buzz-1 {
421
  border-color: #228b22; /* Darker and slightly muted green */
422
  }
423
+ .tossup-question {
424
  line-height: 1.7;
425
  padding: 5px;
426
  margin-left: 4px;
 
429
  border-radius: 8px;
430
  margin-bottom: 10px;
431
  }
432
+
433
+ /* Tooltip styles */
434
+ .tooltip {
435
+ display: none;
436
+ position: fixed; /* Changed to fixed for better positioning */
437
+ padding: 12px 16px;
438
+ border-radius: 8px;
439
+ font-size: 13px;
440
+ white-space: normal;
441
+ z-index: 1000;
442
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
443
+ min-width: 300px;
444
+ max-width: 400px;
445
+ backdrop-filter: blur(4px);
446
+ border: 1px solid rgba(255, 255, 255, 0.2);
447
+ }
448
+
449
+ .tooltip-content {
450
+ color: #2c3e50; /* Darker text for better readability */
451
+ }
452
+
453
+ .tooltip-content div {
454
+ margin: 4px 0;
455
+ line-height: 1.4;
456
+ }
457
+
458
+ /* When hovering over a token, show its tooltip */
459
+ .token:hover .tooltip {
460
+ display: block;
461
+ }
462
+
463
+ /* Add a small arrow to the tooltip */
464
+ .tooltip::after {
465
+ content: '';
466
+ position: absolute;
467
+ bottom: -8px;
468
+ left: 50%;
469
+ transform: translateX(-50%);
470
+ border-left: 8px solid transparent;
471
+ border-right: 8px solid transparent;
472
+ border-top: 8px solid currentColor;
473
+ }
474
+ """
475
+
476
+ css_bonus = """
477
+ .qid-selector {
478
+ box-shadow: 0 0 0 0 !important;
479
+ }
480
+ .qid-selector input {
481
+ border-radius: 12px !important;
482
+ }
483
+ .bonus-header-row {
484
+ align-items: flex-end;
485
+ }
486
+ .bonus-card {
487
+ background-color: var(--card-bg-color);
488
+ border-radius: 12px;
489
+ padding: 12px;
490
+ margin: 0px 0px;
491
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
492
+ }
493
+
494
+ .bonus-leadin {
495
+ font-size: 14px;
496
+ font-weight: 500;
497
+ margin-bottom: 12px;
498
+ line-height: 1.5;
499
+ }
500
+
501
+ .bonus-part {
502
+ background-color: var(--answer-bg-color);
503
+ border-radius: 8px;
504
+ padding: 12px;
505
+ margin: 8px 0;
506
+ }
507
+
508
+ .bonus-part-number {
509
+ font-weight: 600;
510
+ color: #666;
511
+ margin-bottom: 4px;
512
+ }
513
+
514
+ .bonus-part-text {
515
+ margin-bottom: 8px;
516
+ line-height: 1.5;
517
+ }
518
+
519
+ .bonus-answer {
520
+ background-color: #fff5f5;
521
+ border-radius: 6px;
522
+ padding: 8px 12px;
523
+ margin-top: 8px;
524
+ font-size: 14px;
525
+ border-left: 3px solid #ff6b6b;
526
+ }
527
+
528
+ .bonus-answer-label {
529
+ font-weight: 500;
530
+ color: #666;
531
+ margin-bottom: 4px;
532
+ }
533
+
534
+ .bonus-answer-text {
535
+ color: #333;
536
+ }
537
+
538
+ .bonus-container {
539
+ max-width: 800px;
540
+ margin: 0 auto;
541
+ padding-left: 8px;
542
+ padding-right: 8px;
543
+ }
544
  """