from __future__ import annotations from typing import TYPE_CHECKING, Any, NamedTuple import gradio as gr if TYPE_CHECKING: from collections.abc import Callable, Sequence class MutableCheckBoxGroupEntry(NamedTuple): """Entry of the mutable checkbox group.""" name: str value: str class MutableCheckBoxGroup(gr.Blocks): """Check box group with controls to add or remove values.""" def __init__( self, values: list[MutableCheckBoxGroupEntry] | None = None, label: str = "Extendable List", new_value_label: str = "New Item Value", new_name_label: str = "New Item Name", new_value_placeholder: str = "New item value ...", new_name_placeholder: str = "New item name, if not given value will be used...", on_change: Callable[[Sequence[MutableCheckBoxGroupEntry]], None] | None = None, ) -> None: super().__init__() self.values = values or [] self.label = label self.new_value_label = new_value_label self.new_name_label = new_name_label self.new_value_placeholder = new_value_placeholder self.new_name_placeholder = new_name_placeholder self.on_change = on_change self._build_interface() def _build_interface(self) -> None: # Custom CSS for vertical checkbox layout self.style = """ #vertical-container .wrap { display: flex; flex-direction: column; gap: 8px; } #vertical-container .wrap label { display: flex; align-items: center; gap: 8px; } """ with self: gr.Markdown(f"### {self.label}") # Store items in state self.state = gr.State(self.values) # Input row with gr.Row(): self.input_value = gr.Textbox( label=self.new_value_label, placeholder=self.new_value_placeholder, scale=3, ) self.input_name = gr.Textbox( label=self.new_name_label, placeholder=self.new_name_placeholder, scale=2, ) with gr.Row(): self.add_btn = gr.Button( "Add", variant="primary", scale=1, ) self.delete_btn = gr.Button( "Delete Selected", variant="stop", scale=1, ) # Vertical checkbox group self.items_group = gr.CheckboxGroup( choices=self.values, label="Items", elem_id="vertical-container", container=True, ) # Set up event handlers self.add_btn.click( self._add_item, inputs=[self.state, self.input_value, self.input_name], outputs=[ self.state, self.items_group, self.input_value, self.input_name, ], ) self.delete_btn.click( self._delete_selected, inputs=[self.state, self.items_group], outputs=[self.state, self.items_group], ) def get_values(self) -> Sequence[str]: """Get check box values.""" return self.state.value def _add_item( self, items: list[MutableCheckBoxGroupEntry], new_value: str, new_name: str, ) -> tuple[list[MutableCheckBoxGroupEntry], dict[str, Any], str, str]: if not new_name: new_name = new_value if new_value: if any(entry.name == new_name for entry in items): raise gr.Error( f"Entry with name '{new_name}' already exists", duration=10, ) if any(entry.value == new_value for entry in items): raise gr.Error( f"Entry with value '{new_value}' already exists", duration=10, ) items = [*items, MutableCheckBoxGroupEntry(new_name, new_value)] if self.on_change: self.on_change(items) # State, checkbox, input_value, input_name return items, gr.update(choices=items), "", "" # State, checkbox, input_value, input_name return items, gr.update(), "", "" def _delete_selected( self, items: list[MutableCheckBoxGroupEntry], selected: list[str], ) -> tuple[list[MutableCheckBoxGroupEntry], dict[str, Any]]: updated_items = [item for item in items if item.value not in selected] if self.on_change: self.on_change(updated_items) return updated_items, gr.update(choices=updated_items, value=[])