cfy / app /ui /layout.py
killvung's picture
Initial commit
2cd1b38
Raw History Blame Contribute Delete
6.24 kB
"""Review UI component tree (Gradio Blocks layout)."""
from __future__ import annotations
from dataclasses import dataclass, field
import gradio as gr
from app.render import CANDIDATE_COUNT, OUTPUT_KEYS
from app.styles import (
ACCEPTED_COUNT_INITIAL,
APP_TITLE,
BTN_ALL_DONE,
BTN_CONTINUE,
BTN_GO_BACK,
BTN_PREVIOUS,
BTN_SELECT,
CAT_SUBTEXT,
COMPLETE_HEADING,
PRIMARY_INSTRUCTION,
)
def _candidate_image() -> gr.Image:
return gr.Image(
interactive=False,
show_label=False,
container=False,
buttons=[],
elem_classes=["candidate-photo"],
)
def _build_card_row(
slots: tuple[int, ...],
) -> tuple[list[gr.Image], list[gr.Column], list[gr.Button]]:
images: list[gr.Image] = []
cards: list[gr.Column] = []
select_buttons: list[gr.Button] = []
with gr.Row(elem_classes=["grid-row"]):
for slot in slots:
with gr.Column(elem_classes=["card-col"]) as card_col:
cards.append(card_col)
gr.Markdown(f"**Candidate {slot}**", elem_classes=["candidate-label"])
images.append(_candidate_image())
select_buttons.append(
gr.Button(BTN_SELECT, elem_classes=["select-btn", "select-btn-off"])
)
return images, cards, select_buttons
@dataclass
class ReviewUI:
"""Registry of all Gradio components for the review app."""
ctx: gr.State
task: gr.State
selections: gr.State
error_panel: gr.Markdown
review_screen: gr.Column
complete_screen: gr.Column
cat_heading: gr.Markdown
progress: gr.Markdown
images: list[gr.Image] = field(default_factory=list)
cards: list[gr.Column] = field(default_factory=list)
select_buttons: list[gr.Button] = field(default_factory=list)
accepted_count: gr.Markdown | None = None
status_message: gr.Markdown | None = None
primary_btn: gr.Button | None = None
prev_btn: gr.Button | None = None
all_done_nav_btn: gr.Button | None = None
all_done_extra_btn: gr.Button | None = None
go_back_btn: gr.Button | None = None
_components: dict[str, gr.components.Component] = field(default_factory=dict, repr=False)
@property
def state_inputs(self) -> list[gr.State]:
return [self.ctx, self.task, self.selections]
@property
def outputs(self) -> list[gr.components.Component]:
return [self._components[key] for key in OUTPUT_KEYS]
@classmethod
def build(cls) -> ReviewUI:
ctx_state = gr.State(None)
task_state = gr.State(None)
selections_state = gr.State([False] * CANDIDATE_COUNT)
gr.Markdown(f"# {APP_TITLE}", elem_classes=["review-header"])
gr.Markdown(PRIMARY_INSTRUCTION, elem_classes=["review-header"])
error_panel = gr.Markdown("", elem_classes=["error-banner"], visible=False)
with gr.Column(visible=False) as review_screen:
with gr.Column(elem_classes=["review-body"]):
with gr.Column(elem_classes=["cat-block"]):
cat_heading = gr.Markdown("")
gr.Markdown(CAT_SUBTEXT)
progress = gr.Markdown("")
row1_images, row1_cards, row1_buttons = _build_card_row((1, 2))
row2_images, row2_cards, row2_buttons = _build_card_row((3, 4))
images = row1_images + row2_images
cards = row1_cards + row2_cards
select_buttons = row1_buttons + row2_buttons
accepted_count = gr.Markdown(
ACCEPTED_COUNT_INITIAL, elem_classes=["accepted-count"]
)
status_message = gr.Markdown("", elem_classes=["status-message"])
with gr.Row(elem_classes=["actions-row"]):
prev_btn = gr.Button(
BTN_PREVIOUS, elem_classes=["secondary-btn"], visible=False
)
primary_btn = gr.Button(BTN_CONTINUE, elem_classes=["primary-btn"])
all_done_nav_btn = gr.Button(
BTN_ALL_DONE, elem_classes=["secondary-btn"], visible=False
)
all_done_extra_btn = gr.Button(
BTN_ALL_DONE, elem_classes=["all-done-row"], visible=False
)
with gr.Column(visible=False) as complete_screen:
gr.Markdown(COMPLETE_HEADING, elem_classes=["complete-box"])
with gr.Row(elem_classes=["complete-actions"]):
go_back_btn = gr.Button(BTN_GO_BACK, elem_classes=["secondary-btn"])
components = {
"ctx": ctx_state,
"task": task_state,
"selections": selections_state,
"error_panel": error_panel,
"review_screen": review_screen,
"complete_screen": complete_screen,
"cat_heading": cat_heading,
"progress": progress,
**{f"image_{index}": images[index] for index in range(CANDIDATE_COUNT)},
**{f"card_{index}": cards[index] for index in range(CANDIDATE_COUNT)},
**{f"select_{index}": select_buttons[index] for index in range(CANDIDATE_COUNT)},
"accepted_count": accepted_count,
"status_message": status_message,
"primary_btn": primary_btn,
"prev_btn": prev_btn,
"all_done_nav_btn": all_done_nav_btn,
"all_done_extra_btn": all_done_extra_btn,
}
return cls(
ctx=ctx_state,
task=task_state,
selections=selections_state,
error_panel=error_panel,
review_screen=review_screen,
complete_screen=complete_screen,
cat_heading=cat_heading,
progress=progress,
images=images,
cards=cards,
select_buttons=select_buttons,
accepted_count=accepted_count,
status_message=status_message,
primary_btn=primary_btn,
prev_btn=prev_btn,
all_done_nav_btn=all_done_nav_btn,
all_done_extra_btn=all_done_extra_btn,
go_back_btn=go_back_btn,
_components=components,
)