File size: 7,550 Bytes
2cd1b38 08a85f9 2cd1b38 08a85f9 2cd1b38 08a85f9 2cd1b38 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | """Map review session state to Gradio component updates."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any
import gradio as gr
from app.db import SupabaseReviewStore
from app.images import image_display_value
from app.models import EvaluationTask
from app.review_session import ReviewContext, all_tasks_submitted
CANDIDATE_COUNT = 4
OUTPUT_KEYS: tuple[str, ...] = (
"ctx",
"task",
"selections",
"error_panel",
"review_screen",
"complete_screen",
"cat_heading",
"progress",
*(f"image_{index}" for index in range(CANDIDATE_COUNT)),
*(f"card_{index}" for index in range(CANDIDATE_COUNT)),
*(f"select_{index}" for index in range(CANDIDATE_COUNT)),
"accepted_count",
"status_message",
"primary_btn",
"prev_btn",
"all_done_nav_btn",
"all_done_extra_btn",
)
@dataclass
class ReviewRenderer:
store: SupabaseReviewStore
@staticmethod
def toggle_selection(selections: list[bool], index: int) -> list[bool]:
updated = list(selections)
updated[index] = not updated[index]
return updated
def progress_md(self, ctx: ReviewContext) -> str:
if not ctx.task_ids:
return ""
parts: list[str] = []
for index, task_id in enumerate(ctx.task_ids):
if task_id in ctx.completed_task_ids:
parts.append('<span class="done">β</span>')
elif index == ctx.current_index:
parts.append('<span class="here">β</span>')
else:
parts.append("β")
return f'<p class="progress">{"".join(parts)}</p>'
def _card_update(self, selected: bool) -> gr.Update:
return gr.update(elem_classes=["card-col", "selected"] if selected else ["card-col"])
def _select_update(self, selected: bool) -> gr.Update:
return gr.update(
value="β Selected" if selected else "Select",
elem_classes=["select-btn", "select-btn-on" if selected else "select-btn-off"],
)
def build_toggle_updates(
self,
index: int,
ctx: ReviewContext,
selections: list[bool],
) -> dict[str, Any]:
"""Minimal updates for Select toggle β no images, no DB, no full re-render."""
ctx.status_message = ""
updates: dict[str, Any] = {
"ctx": ctx,
"selections": selections,
"status_message": gr.update(value="", elem_classes=["status-message"]),
"accepted_count": gr.update(
value=f"Accepted so far β **{sum(selections)} / 4**"
),
f"card_{index}": self._card_update(selections[index]),
f"select_{index}": self._select_update(selections[index]),
}
return updates
def render_toggle(
self,
index: int,
ctx: ReviewContext,
selections: list[bool],
) -> tuple:
return self.pack(self.build_toggle_updates(index, ctx, selections))
def _empty_candidates(self, updates: dict[str, Any]) -> None:
for index in range(CANDIDATE_COUNT):
updates[f"image_{index}"] = gr.update(value=None)
updates[f"card_{index}"] = gr.update(elem_classes=["card-col"])
updates[f"select_{index}"] = gr.update(
value="Select", elem_classes=["select-btn", "select-btn-off"]
)
def _candidate_updates(
self,
updates: dict[str, Any],
task: EvaluationTask,
selections: list[bool],
) -> None:
for index, candidate in enumerate(task.candidates):
display = image_display_value(candidate.local_path, candidate.storage_url)
selected = selections[index] if index < len(selections) else False
if display is None:
ref = candidate.storage_url or candidate.local_path or "unknown"
updates[f"image_{index}"] = gr.update(
value=None, label=f"Missing image: {ref}"
)
else:
updates[f"image_{index}"] = gr.update(value=display, label=None)
updates[f"card_{index}"] = self._card_update(selected)
updates[f"select_{index}"] = self._select_update(selected)
def _status_update(self, ctx: ReviewContext) -> gr.Update:
classes = ["status-message"]
if ctx.status_message.startswith(("Could not", "These")):
classes.append("status-error")
elif ctx.status_message:
classes.append("status-ok")
return gr.update(value=ctx.status_message, elem_classes=classes)
def build_updates(
self,
ctx: ReviewContext,
task: EvaluationTask | None,
selections: list[bool],
) -> dict[str, Any]:
updates: dict[str, Any] = {"ctx": ctx, "task": task, "selections": selections}
if ctx.screen == "error":
updates.update(
{
"error_panel": gr.update(value=ctx.error_message, visible=True),
"review_screen": gr.update(visible=False),
"complete_screen": gr.update(visible=False),
}
)
return updates
updates["error_panel"] = gr.update(value="", visible=False)
if ctx.screen == "complete":
updates.update(
{
"review_screen": gr.update(visible=False),
"complete_screen": gr.update(visible=True),
}
)
return updates
updates["complete_screen"] = gr.update(visible=False)
updates["review_screen"] = gr.update(visible=True)
if task is None:
updates["cat_heading"] = gr.update(value="")
updates["progress"] = gr.update(value="")
updates["accepted_count"] = gr.update(value="Accepted so far β **0 / 4**")
updates["status_message"] = gr.update(value="")
self._empty_candidates(updates)
updates["primary_btn"] = gr.update(value="Continue")
updates["prev_btn"] = gr.update(visible=False)
updates["all_done_nav_btn"] = gr.update(visible=False)
updates["all_done_extra_btn"] = gr.update(visible=False)
return updates
self._candidate_updates(updates, task, selections)
accepted = sum(selections)
all_done = all_tasks_submitted(self.store, ctx)
on_first = ctx.current_index == 0
on_last = ctx.current_index >= len(ctx.task_ids) - 1
updates["cat_heading"] = gr.update(value=f"### {task.cat_display_name}")
updates["progress"] = gr.update(value=self.progress_md(ctx))
updates["accepted_count"] = gr.update(value=f"Accepted so far β **{accepted} / 4**")
updates["primary_btn"] = gr.update(value="Finish" if on_last else "Continue")
updates["prev_btn"] = gr.update(visible=not on_first)
updates["all_done_nav_btn"] = gr.update(visible=on_first and all_done)
updates["all_done_extra_btn"] = gr.update(visible=(not on_first) and all_done)
updates["status_message"] = self._status_update(ctx)
return updates
def pack(self, updates: dict[str, Any]) -> tuple:
return tuple(updates.get(key, gr.update()) for key in OUTPUT_KEYS)
def render(
self,
ctx: ReviewContext,
task: EvaluationTask | None,
selections: list[bool],
) -> tuple:
return self.pack(self.build_updates(ctx, task, selections))
|