Files
chainlit--chainlit/backend/chainlit/input_widget.py
T
wehub-resource-sync b7f52be4c9
Copilot Setup Steps / copilot-setup-steps (push) Waiting to run
CI / Run CI (push) Has been cancelled
CI / check-backend (push) Has been cancelled
CI / check-frontend (push) Has been cancelled
CI / tests (push) Has been cancelled
CI / e2e-tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:48:47 +08:00

426 lines
12 KiB
Python

from abc import abstractmethod
from datetime import date
from typing import Any, Dict, List, Literal, Optional
from pydantic import Field
from pydantic.dataclasses import dataclass
from chainlit.types import InputWidgetType
@dataclass
class InputWidget:
id: str
label: str
initial: Any = None
tooltip: Optional[str] = None
description: Optional[str] = None
disabled: Optional[bool] = False
def __post_init__(
self,
) -> None:
if not self.id or not self.label:
raise ValueError("Must provide key and label to load InputWidget")
@abstractmethod
def to_dict(self) -> Dict[str, Any]:
pass
@dataclass
class Switch(InputWidget):
"""Useful to create a switch input."""
type: InputWidgetType = "switch"
initial: bool = False
def to_dict(self) -> Dict[str, Any]:
return {
"type": self.type,
"id": self.id,
"label": self.label,
"initial": self.initial,
"tooltip": self.tooltip,
"description": self.description,
"disabled": self.disabled,
}
@dataclass
class Slider(InputWidget):
"""Useful to create a slider input."""
type: InputWidgetType = "slider"
initial: float = 0
min: float = 0
max: float = 10
step: float = 1
def to_dict(self) -> Dict[str, Any]:
return {
"type": self.type,
"id": self.id,
"label": self.label,
"initial": self.initial,
"min": self.min,
"max": self.max,
"step": self.step,
"tooltip": self.tooltip,
"description": self.description,
"disabled": self.disabled,
}
@dataclass
class Select(InputWidget):
"""Useful to create a select input."""
type: InputWidgetType = "select"
initial: Optional[str] = None
initial_index: Optional[int] = None
initial_value: Optional[str] = None
values: List[str] = Field(default_factory=list)
items: Dict[str, str] = Field(default_factory=dict)
def __post_init__(
self,
) -> None:
super().__post_init__()
if not self.values and not self.items:
raise ValueError("Must provide values or items to create a Select")
if self.values and self.items:
raise ValueError(
"You can only provide either values or items to create a Select"
)
if not self.values and self.initial_index is not None:
raise ValueError(
"Initial_index can only be used in combination with values to create a Select"
)
if self.items:
self.initial = self.initial_value
elif self.values:
self.items = {value: value for value in self.values}
self.initial = (
self.values[self.initial_index]
if self.initial_index is not None
else self.initial_value
)
def to_dict(self) -> Dict[str, Any]:
return {
"type": self.type,
"id": self.id,
"label": self.label,
"initial": self.initial,
"items": [
{"label": id, "value": value} for id, value in self.items.items()
],
"tooltip": self.tooltip,
"description": self.description,
"disabled": self.disabled,
}
@dataclass
class TextInput(InputWidget):
"""Useful to create a text input."""
type: InputWidgetType = "textinput"
initial: Optional[str] = None
placeholder: Optional[str] = None
multiline: bool = False
def to_dict(self) -> Dict[str, Any]:
return {
"type": self.type,
"id": self.id,
"label": self.label,
"initial": self.initial,
"placeholder": self.placeholder,
"tooltip": self.tooltip,
"description": self.description,
"multiline": self.multiline,
"disabled": self.disabled,
}
@dataclass
class NumberInput(InputWidget):
"""Useful to create a number input."""
type: InputWidgetType = "numberinput"
initial: Optional[float] = None
placeholder: Optional[str] = None
def to_dict(self) -> Dict[str, Any]:
return {
"type": self.type,
"id": self.id,
"label": self.label,
"initial": self.initial,
"placeholder": self.placeholder,
"tooltip": self.tooltip,
"description": self.description,
"disabled": self.disabled,
}
@dataclass
class Tags(InputWidget):
"""Useful to create an input for an array of strings."""
type: InputWidgetType = "tags"
initial: List[str] = Field(default_factory=list)
values: List[str] = Field(default_factory=list)
def to_dict(self) -> Dict[str, Any]:
return {
"type": self.type,
"id": self.id,
"label": self.label,
"initial": self.initial,
"tooltip": self.tooltip,
"description": self.description,
"disabled": self.disabled,
}
@dataclass
class MultiSelect(InputWidget):
"""Useful to create a multi-select input."""
type: InputWidgetType = "multiselect"
initial: List[str] = Field(default_factory=list)
values: List[str] = Field(default_factory=list)
items: Dict[str, str] = Field(default_factory=dict)
def __post_init__(
self,
) -> None:
super().__post_init__()
if not self.values and not self.items:
raise ValueError("Must provide values or items to create a MultiSelect")
if self.values and self.items:
raise ValueError(
"You can only provide either values or items to create a MultiSelect"
)
if self.values:
self.items = {value: value for value in self.values}
def to_dict(self) -> Dict[str, Any]:
return {
"type": self.type,
"id": self.id,
"label": self.label,
"initial": self.initial,
"items": [
{"label": id, "value": value} for id, value in self.items.items()
],
"tooltip": self.tooltip,
"description": self.description,
"disabled": self.disabled,
}
@dataclass
class Checkbox(InputWidget):
"""Useful to create a checkbox input."""
type: InputWidgetType = "checkbox"
initial: bool = False
def to_dict(self) -> Dict[str, Any]:
return {
"type": self.type,
"id": self.id,
"label": self.label,
"initial": self.initial,
"tooltip": self.tooltip,
"description": self.description,
"disabled": self.disabled,
}
@dataclass
class RadioGroup(InputWidget):
"""Useful to create a radio button input."""
type: InputWidgetType = "radio"
initial: Optional[str] = None
initial_index: Optional[int] = None
initial_value: Optional[str] = None
values: List[str] = Field(default_factory=list)
items: Dict[str, str] = Field(default_factory=dict)
def __post_init__(
self,
) -> None:
super().__post_init__()
if not self.values and not self.items:
raise ValueError("Must provide values or items to create a RadioButton")
if self.values and self.items:
raise ValueError(
"You can only provide either values or items to create a RadioButton"
)
if not self.values and self.initial_index is not None:
raise ValueError(
"Initial_index can only be used in combination with values to create a RadioButton"
)
if self.items:
self.initial = self.initial_value
elif self.values:
self.items = {value: value for value in self.values}
self.initial = (
self.values[self.initial_index]
if self.initial_index is not None
else self.initial_value
)
def to_dict(self) -> Dict[str, Any]:
return {
"type": self.type,
"id": self.id,
"label": self.label,
"initial": self.initial,
"items": [
{"label": id, "value": value} for id, value in self.items.items()
],
"tooltip": self.tooltip,
"description": self.description,
"disabled": self.disabled,
}
@dataclass
class Tab:
id: str
label: str
inputs: list[InputWidget] = Field(default_factory=list, exclude=True)
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"label": self.label,
"inputs": [input.to_dict() for input in self.inputs],
}
@dataclass
class DatePicker(InputWidget):
"""
Datepicker input widget.
Supports both single date and date range selection.
"""
type: InputWidgetType = "datepicker"
mode: Literal["single", "range"] = "single"
initial: str | date | tuple[str | date, str | date] | None = None
min_date: str | date | None = None
max_date: str | date | None = None
format: str | None = None
"""date-fns format string"""
placeholder: str | None = None
"""Placeholder to use when no date is selected"""
def __post_init__(self) -> None:
super().__post_init__()
if self.mode not in ("single", "range"):
raise ValueError("mode must be 'single' or 'range'")
if (
self.mode == "range"
and self.initial is not None
and not isinstance(self.initial, tuple)
):
raise ValueError("'initial' must be a tuple for range mode")
(initial_start, initial_end), min_date, max_date = (
[
DatePicker._validate_iso_format(date, "initial")
for date in (
self.initial
if isinstance(self.initial, tuple)
else [self.initial, None]
)
],
DatePicker._validate_iso_format(self.min_date, "min_date"),
DatePicker._validate_iso_format(self.max_date, "max_date"),
)
if self.mode == "range":
self._validate_range(initial_start, initial_end, "initial")
self._validate_range(min_date, max_date, "[min_date, max_date]")
# Validate that initial value(s) are within min_date and max_date bounds
for d in [initial_start, initial_end]:
if d is not None and (
(min_date is not None and d < min_date)
or (max_date is not None and d > max_date)
):
raise ValueError(
"'initial' must be within 'min_date' and 'max_date' bounds"
)
@staticmethod
def _validate_range(
start: date | None,
end: date | None,
field_name: str,
) -> None:
if start is not None and end is not None and start > end:
raise ValueError(
f"'{field_name}' range is invalid, start must be before end."
)
@staticmethod
def _validate_iso_format(
date_value: str | date | None, field_name: str
) -> date | None:
if isinstance(date_value, str):
try:
return date.fromisoformat(date_value)
except ValueError as e:
raise ValueError(f"'{field_name}' must be in ISO format") from e
return date_value
@staticmethod
def _format_date(date_value: str | date | None) -> str | None:
if isinstance(date_value, date):
return date_value.isoformat()
return date_value
def to_dict(self) -> dict[str, Any]:
return {
"type": self.type,
"id": self.id,
"label": self.label,
"tooltip": self.tooltip,
"description": self.description,
"mode": self.mode,
"initial": (
self._format_date(self.initial[0]),
self._format_date(self.initial[1]),
)
if isinstance(self.initial, tuple)
else DatePicker._format_date(self.initial),
"min_date": DatePicker._format_date(self.min_date),
"max_date": DatePicker._format_date(self.max_date),
"format": self.format,
"placeholder": self.placeholder,
"disabled": self.disabled,
}