openai/chatkit-python

Public

mirrored from https://github.com/openai/chatkit-pythonAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.5.0

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

chatkit/widgets.py

1190lines · modecode

1from __future__ import annotations
2
3import inspect
4import json
5from datetime import datetime
6from pathlib import Path
7from typing import Annotated, Any, Literal
8
9from jinja2 import Environment, StrictUndefined, Template
10from pydantic import (
11 BaseModel,
12 ConfigDict,
13 Field,
14 model_serializer,
15)
16from typing_extensions import NotRequired, TypedDict, deprecated
17
18from .actions import ActionConfig
19from .icons import IconName
20
21_jinja_env = Environment(undefined=StrictUndefined)
22
23_direct_usage_of_named_widget_types_deprecated = deprecated(
24 "Direct usage of named widget classes is deprecated. "
25 "Use WidgetTemplate to build widgets from .widget files instead. "
26 "Visit https://widgets.chatkit.studio/ to author widget files."
27)
28
29
30@_direct_usage_of_named_widget_types_deprecated
31class ThemeColor(TypedDict):
32 """Color values for light and dark themes."""
33
34 dark: str
35 """Color to use when the theme is dark."""
36 light: str
37 """Color to use when the theme is light."""
38
39
40@_direct_usage_of_named_widget_types_deprecated
41class Spacing(TypedDict):
42 """Shorthand spacing values applied to a widget."""
43
44 top: NotRequired[float | str]
45 """Top spacing; accepts a spacing unit or CSS string."""
46 right: NotRequired[float | str]
47 """Right spacing; accepts a spacing unit or CSS string."""
48 bottom: NotRequired[float | str]
49 """Bottom spacing; accepts a spacing unit or CSS string."""
50 left: NotRequired[float | str]
51 """Left spacing; accepts a spacing unit or CSS string."""
52 x: NotRequired[float | str]
53 """Horizontal spacing; accepts a spacing unit or CSS string."""
54 y: NotRequired[float | str]
55 """Vertical spacing; accepts a spacing unit or CSS string."""
56
57
58@_direct_usage_of_named_widget_types_deprecated
59class Border(TypedDict):
60 """Border style definition for an edge."""
61
62 size: int
63 """Thickness of the border in px."""
64 color: NotRequired[str | ThemeColor]
65 """Border color; accepts border color token, a primitive color token, a CSS string, or theme-aware `{ light, dark }`.
66
67 Valid tokens: `default` `subtle` `strong`
68
69 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
70 """
71 style: NotRequired[
72 Literal[
73 "solid", "dashed", "dotted", "double", "groove", "ridge", "inset", "outset"
74 ]
75 ]
76 """Border line style."""
77
78
79@_direct_usage_of_named_widget_types_deprecated
80class Borders(TypedDict):
81 """Composite border configuration applied across edges."""
82
83 top: NotRequired[int | Border]
84 """Top border or thickness in px."""
85 right: NotRequired[int | Border]
86 """Right border or thickness in px."""
87 bottom: NotRequired[int | Border]
88 """Bottom border or thickness in px."""
89 left: NotRequired[int | Border]
90 """Left border or thickness in px."""
91 x: NotRequired[int | Border]
92 """Horizontal borders or thickness in px."""
93 y: NotRequired[int | Border]
94 """Vertical borders or thickness in px."""
95
96
97@_direct_usage_of_named_widget_types_deprecated
98class MinMax(TypedDict):
99 """Integer minimum/maximum bounds."""
100
101 min: NotRequired[int]
102 """Minimum value (inclusive)."""
103 max: NotRequired[int]
104 """Maximum value (inclusive)."""
105
106
107@_direct_usage_of_named_widget_types_deprecated
108class EditableProps(TypedDict):
109 """Editable field options for text widgets."""
110
111 name: str
112 """The name of the form control field used when submitting forms."""
113 autoFocus: NotRequired[bool]
114 """Autofocus the editable input when it appears."""
115 autoSelect: NotRequired[bool]
116 """Select all text on focus."""
117 autoComplete: NotRequired[str]
118 """Native autocomplete hint for the input."""
119 allowAutofillExtensions: NotRequired[bool]
120 """Allow browser password/autofill extensions."""
121 pattern: NotRequired[str]
122 """Regex pattern for input validation."""
123 placeholder: NotRequired[str]
124 """Placeholder text for the editable input."""
125 required: NotRequired[bool]
126 """Mark the editable input as required."""
127
128
129RadiusValue = Literal[
130 "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "full", "100%", "none"
131]
132"""Allowed corner radius tokens."""
133
134TextAlign = Literal["start", "center", "end"]
135"""Horizontal text alignment options."""
136
137TextSize = Literal["xs", "sm", "md", "lg", "xl"]
138"""Body text size tokens."""
139
140IconSize = Literal["xs", "sm", "md", "lg", "xl", "2xl", "3xl"]
141"""Icon size tokens."""
142
143TitleSize = Literal["sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl"]
144"""Title text size tokens."""
145
146CaptionSize = Literal["sm", "md", "lg"]
147"""Caption text size tokens."""
148
149Alignment = Literal["start", "center", "end", "baseline", "stretch"]
150"""Flexbox alignment options."""
151Justification = Literal[
152 "start", "center", "end", "between", "around", "evenly", "stretch"
153]
154"""Flexbox justification options."""
155
156ControlVariant = Literal["solid", "soft", "outline", "ghost"]
157"""Button and input style variants."""
158ControlSize = Literal["3xs", "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl"]
159"""Button and input size variants."""
160
161
162def _drop_none(x):
163 """Recursively remove ``None`` values when serializing widgets."""
164 if isinstance(x, dict):
165 return {
166 k: _drop_none(v) for k, v in x.items() if k == "children" or v is not None
167 }
168 if isinstance(x, list):
169 return [_drop_none(v) for v in x if v is not None]
170 return x
171
172
173class WidgetComponentBase(BaseModel):
174 """Base Pydantic model for all ChatKit widget components."""
175
176 model_config = ConfigDict(serialize_by_alias=True)
177
178 key: str | None = None
179 id: str | None = None
180 type: str = Field(...)
181
182 # For nested model dumps (e.g. if Widget is not the top-level model)
183 @model_serializer(mode="wrap")
184 def serialize(self, next_):
185 dumped = next_(self)
186 # Recursively filter out None values when serialized.
187 # Do this explicitly instead of overriding model_dump_json and model_dump;
188 # the overrides will not be invoked unless the widget is the top-level model.
189 dumped = _drop_none(dumped)
190 # include type even when exlude_defaults is True
191 if isinstance(dumped, dict):
192 dumped["type"] = self.type
193
194 return dumped
195
196
197@_direct_usage_of_named_widget_types_deprecated
198class WidgetStatusWithFavicon(TypedDict):
199 """Widget status representation using a favicon."""
200
201 text: str
202 """Status text to display."""
203 favicon: NotRequired[str]
204 """URL of a favicon to render at the start of the status."""
205 frame: NotRequired[bool]
206 """Show a frame around the favicon for contrast."""
207
208
209@_direct_usage_of_named_widget_types_deprecated
210class WidgetStatusWithIcon(TypedDict):
211 """Widget status representation using an icon."""
212
213 text: str
214 """Status text to display."""
215 icon: NotRequired[WidgetIcon]
216 """Icon to render at the start of the status."""
217
218
219WidgetStatus = WidgetStatusWithFavicon | WidgetStatusWithIcon
220"""Union for representing widget status messaging."""
221
222
223@_direct_usage_of_named_widget_types_deprecated
224class ListViewItem(WidgetComponentBase):
225 """Single row inside a ``ListView`` component."""
226
227 type: Literal["ListViewItem"] = Field(default="ListViewItem", frozen=True) # pyright: ignore
228 children: list["WidgetComponent"]
229 """Content for the list item."""
230 onClickAction: ActionConfig | None = None
231 """Optional action triggered when the list item is clicked."""
232 gap: int | str | None = None
233 """Gap between children within the list item; spacing unit or CSS string."""
234 align: Alignment | None = None
235 """Y-axis alignment for content within the list item."""
236
237
238@_direct_usage_of_named_widget_types_deprecated
239class ListView(WidgetComponentBase):
240 """Container component for rendering collections of list items."""
241
242 type: Literal["ListView"] = Field(default="ListView", frozen=True) # pyright: ignore
243 children: list[ListViewItem]
244 """Items to render in the list."""
245 limit: int | Literal["auto"] | None = None
246 """Max number of items to show before a "Show more" control."""
247 status: WidgetStatus | None = None
248 """Optional status header displayed above the list."""
249 theme: Literal["light", "dark"] | None = None
250 """Force light or dark theme for this subtree."""
251
252
253@_direct_usage_of_named_widget_types_deprecated
254class CardAction(TypedDict):
255 """Configuration for confirm/cancel actions within a card."""
256
257 label: str
258 """Button label shown in the card footer."""
259 action: ActionConfig
260 """Declarative action dispatched to the host application."""
261
262
263@_direct_usage_of_named_widget_types_deprecated
264class Card(WidgetComponentBase):
265 """Versatile container used for structuring widget content."""
266
267 type: Literal["Card"] = Field(default="Card", frozen=True) # pyright: ignore
268 asForm: bool | None = None
269 """Treat the card as an HTML form so confirm/cancel capture form data."""
270 children: list["WidgetComponent"]
271 """Child components rendered inside the card."""
272 background: str | ThemeColor | None = None
273 """Background color; accepts background color token, a primitive color token, a CSS string, or theme-aware `{ light, dark }`.
274
275 Valid tokens: `surface` `surface-secondary` `surface-tertiary` `surface-elevated` `surface-elevated-secondary`
276
277 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
278 """
279 size: Literal["sm", "md", "lg", "full"] | None = None
280 """Visual size of the card; accepts a size token. No preset default is documented."""
281 padding: float | str | Spacing | None = None
282 """Inner spacing of the card; spacing unit, CSS string, or padding object."""
283 status: WidgetStatus | None = None
284 """Optional status header displayed above the card."""
285 collapsed: bool | None = None
286 """Collapse card body after the main action has completed."""
287 confirm: CardAction | None = None
288 """Confirmation action button shown in the card footer."""
289 cancel: CardAction | None = None
290 """Cancel action button shown in the card footer."""
291 theme: Literal["light", "dark"] | None = None
292 """Force light or dark theme for this subtree."""
293
294
295@_direct_usage_of_named_widget_types_deprecated
296class Markdown(WidgetComponentBase):
297 """Widget rendering Markdown content, optionally streamed."""
298
299 type: Literal["Markdown"] = Field(default="Markdown", frozen=True) # pyright: ignore
300 value: str
301 """Markdown source string to render."""
302 streaming: bool | None = None
303 """Applies streaming-friendly transitions for incremental updates."""
304
305
306@_direct_usage_of_named_widget_types_deprecated
307class Text(WidgetComponentBase):
308 """Widget rendering plain text with typography controls."""
309
310 type: Literal["Text"] = Field(default="Text", frozen=True) # pyright: ignore
311 value: str
312 """Text content to display."""
313 streaming: bool | None = None
314 """Enables streaming-friendly transitions for incremental updates."""
315 italic: bool | None = None
316 """Render text in italic style."""
317 lineThrough: bool | None = None
318 """Render text with a line-through decoration."""
319 color: str | ThemeColor | None = None
320 """
321 Text color; accepts a text color token, a primitive color token, a CSS color string, or a theme-aware `{ light, dark }`.
322
323 Text color tokens: `prose` `primary` `emphasis` `secondary` `tertiary` `success` `warning` `danger`
324
325 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
326 """
327 weight: Literal["normal", "medium", "semibold", "bold"] | None = None
328 """Font weight; accepts a font weight token."""
329 width: float | str | None = None
330 """Constrain the text container width; px or CSS string."""
331 size: TextSize | None = None
332 """Size of the text; accepts a text size token."""
333 textAlign: TextAlign | None = None
334 """Horizontal text alignment."""
335 truncate: bool | None = None
336 """Truncate overflow with ellipsis."""
337 minLines: int | None = None
338 """Reserve space for a minimum number of lines."""
339 maxLines: int | None = None
340 """Limit text to a maximum number of lines (line clamp)."""
341 editable: Literal[False] | EditableProps | None = None
342 """Enable inline editing for this text node."""
343
344
345@_direct_usage_of_named_widget_types_deprecated
346class Title(WidgetComponentBase):
347 """Widget rendering prominent headline text."""
348
349 type: Literal["Title"] = Field(default="Title", frozen=True) # pyright: ignore
350 value: str
351 """Text content to display."""
352 color: str | ThemeColor | None = None
353 """
354 Text color; accepts a text color token, a primitive color token, a CSS color string, or a theme-aware `{ light, dark }`.
355
356 Text color tokens: `prose` `primary` `emphasis` `secondary` `tertiary` `success` `warning` `danger`
357
358 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
359 """
360 weight: Literal["normal", "medium", "semibold", "bold"] | None = None
361 """Font weight; accepts a font weight token."""
362 size: TitleSize | None = None
363 """Size of the title text; accepts a title size token."""
364 textAlign: TextAlign | None = None
365 """Horizontal text alignment."""
366 truncate: bool | None = None
367 """Truncate overflow with ellipsis."""
368 maxLines: int | None = None
369 """Limit text to a maximum number of lines (line clamp)."""
370
371
372@_direct_usage_of_named_widget_types_deprecated
373class Caption(WidgetComponentBase):
374 """Widget rendering supporting caption text."""
375
376 type: Literal["Caption"] = Field(default="Caption", frozen=True) # pyright: ignore
377 value: str
378 """Text content to display."""
379 color: str | ThemeColor | None = None
380 """
381 Text color; accepts a text color token, a primitive color token, a CSS color string, or a theme-aware `{ light, dark }`.
382
383 Text color tokens: `prose` `primary` `emphasis` `secondary` `tertiary` `success` `warning` `danger`
384
385 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
386 """
387 weight: Literal["normal", "medium", "semibold", "bold"] | None = None
388 """Font weight; accepts a font weight token."""
389 size: CaptionSize | None = None
390 """Size of the caption text; accepts a caption size token."""
391 textAlign: TextAlign | None = None
392 """Horizontal text alignment."""
393 truncate: bool | None = None
394 """Truncate overflow with ellipsis."""
395 maxLines: int | None = None
396 """Limit text to a maximum number of lines (line clamp)."""
397
398
399@_direct_usage_of_named_widget_types_deprecated
400class Badge(WidgetComponentBase):
401 """Small badge indicating status or categorization."""
402
403 type: Literal["Badge"] = Field(default="Badge", frozen=True) # pyright: ignore
404 label: str
405 """Text to display inside the badge."""
406 color: (
407 Literal["secondary", "success", "danger", "warning", "info", "discovery"] | None
408 ) = None
409 """Color of the badge; accepts a badge color token."""
410 variant: Literal["solid", "soft", "outline"] | None = None
411 """Visual style of the badge."""
412 size: Literal["sm", "md", "lg"] | None = None
413 """Size of the badge."""
414 pill: bool | None = None
415 """Determines if the badge should be fully rounded (pill)."""
416
417
418@_direct_usage_of_named_widget_types_deprecated
419class BoxBase(BaseModel):
420 """Shared layout props for flexible container widgets."""
421
422 children: list["WidgetComponent"] | None = None
423 """Child components to render inside the container."""
424 align: Alignment | None = None
425 """Cross-axis alignment of children."""
426 justify: Justification | None = None
427 """Main-axis distribution of children."""
428 wrap: Literal["nowrap", "wrap", "wrap-reverse"] | None = None
429 """Wrap behavior for flex items."""
430 flex: int | str | None = None
431 """Flex growth/shrink factor."""
432 gap: int | str | None = None
433 """Gap between direct children; spacing unit or CSS string."""
434 height: float | str | None = None
435 """Explicit height; px or CSS string."""
436 width: float | str | None = None
437 """Explicit width; px or CSS string."""
438 size: float | str | None = None
439 """Shorthand to set both width and height; px or CSS string."""
440 minHeight: int | str | None = None
441 """Minimum height; px or CSS string."""
442 minWidth: int | str | None = None
443 """Minimum width; px or CSS string."""
444 minSize: int | str | None = None
445 """Shorthand to set both minWidth and minHeight; px or CSS string."""
446 maxHeight: int | str | None = None
447 """Maximum height; px or CSS string."""
448 maxWidth: int | str | None = None
449 """Maximum width; px or CSS string."""
450 maxSize: int | str | None = None
451 """Shorthand to set both maxWidth and maxHeight; px or CSS string."""
452 padding: float | str | Spacing | None = None
453 """Inner padding; spacing unit, CSS string, or padding object."""
454 margin: float | str | Spacing | None = None
455 """Outer margin; spacing unit, CSS string, or margin object."""
456 border: int | Border | Borders | None = None
457 """Border applied to the container; px or border object/shorthand."""
458 radius: RadiusValue | None = None
459 """Border radius; accepts a radius token."""
460 background: str | ThemeColor | None = None
461 """Background color; accepts background color token, a primitive color token, a CSS string, or theme-aware `{ light, dark }`.
462
463 Valid tokens: `surface` `surface-secondary` `surface-tertiary` `surface-elevated` `surface-elevated-secondary`
464
465 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
466 """
467 aspectRatio: float | str | None = None
468 """Aspect ratio of the box (e.g., 16/9); number or CSS string."""
469
470
471@_direct_usage_of_named_widget_types_deprecated
472class Box(WidgetComponentBase, BoxBase):
473 """Generic flex container with direction control."""
474
475 type: Literal["Box"] = Field(default="Box", frozen=True) # pyright: ignore
476 direction: Literal["row", "col"] | None = None
477 """Flex direction for content within this container."""
478
479
480@_direct_usage_of_named_widget_types_deprecated
481class Row(WidgetComponentBase, BoxBase):
482 """Horizontal flex container."""
483
484 type: Literal["Row"] = Field(default="Row", frozen=True) # pyright: ignore
485
486
487@_direct_usage_of_named_widget_types_deprecated
488class Col(WidgetComponentBase, BoxBase):
489 """Vertical flex container."""
490
491 type: Literal["Col"] = Field(default="Col", frozen=True) # pyright: ignore
492
493
494@_direct_usage_of_named_widget_types_deprecated
495class Form(WidgetComponentBase, BoxBase):
496 """Form wrapper capable of submitting ``onSubmitAction``."""
497
498 type: Literal["Form"] = Field(default="Form", frozen=True) # pyright: ignore
499 onSubmitAction: ActionConfig | None = None
500 """Action dispatched when the form is submitted."""
501 direction: Literal["row", "col"] | None = None
502 """Flex direction for laying out form children."""
503
504
505@_direct_usage_of_named_widget_types_deprecated
506class Divider(WidgetComponentBase):
507 """Visual divider separating content sections."""
508
509 type: Literal["Divider"] = Field(default="Divider", frozen=True) # pyright: ignore
510 color: str | ThemeColor | None = None
511 """Divider color; accepts border color token, a primitive color token, a CSS string, or theme-aware `{ light, dark }`.
512
513 Valid tokens: `default` `subtle` `strong`
514
515 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
516 """
517 size: int | str | None = None
518 """Thickness of the divider line; px or CSS string."""
519 spacing: int | str | None = None
520 """Outer spacing above and below the divider; spacing unit or CSS string."""
521 flush: bool | None = None
522 """Flush the divider to the container edge, removing surrounding padding."""
523
524
525@_direct_usage_of_named_widget_types_deprecated
526class Icon(WidgetComponentBase):
527 """Icon component referencing a built-in icon name."""
528
529 type: Literal["Icon"] = Field(default="Icon", frozen=True) # pyright: ignore
530 name: WidgetIcon
531 """Name of the icon to display."""
532 color: str | ThemeColor | None = None
533 """
534 Icon color; accepts a text color token, a primitive color token, a CSS color string, or a theme-aware `{ light, dark }`.
535
536 Text color tokens: `prose` `primary` `emphasis` `secondary` `tertiary` `success` `warning` `danger`
537
538 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
539 """
540 size: IconSize | None = None
541 """Size of the icon; accepts an icon size token."""
542
543
544@_direct_usage_of_named_widget_types_deprecated
545class Image(WidgetComponentBase):
546 """Image component with sizing and fitting controls."""
547
548 type: Literal["Image"] = Field(default="Image", frozen=True) # pyright: ignore
549 src: str
550 """Image URL source."""
551 alt: str | None = None
552 """Alternate text for accessibility."""
553 fit: Literal["cover", "contain", "fill", "scale-down", "none"] | None = None
554 """How the image should fit within the container."""
555 position: (
556 Literal[
557 "top left",
558 "top",
559 "top right",
560 "left",
561 "center",
562 "right",
563 "bottom left",
564 "bottom",
565 "bottom right",
566 ]
567 | None
568 ) = None
569 """Focal position of the image within the container."""
570 radius: RadiusValue | None = None
571 """Border radius; accepts a radius token."""
572 frame: bool | None = None
573 """Draw a subtle frame around the image."""
574 flush: bool | None = None
575 """Flush the image to the container edge, removing surrounding padding."""
576 height: int | str | None = None
577 """Explicit height; px or CSS string."""
578 width: int | str | None = None
579 """Explicit width; px or CSS string."""
580 size: int | str | None = None
581 """Shorthand to set both width and height; px or CSS string."""
582 minHeight: int | str | None = None
583 """Minimum height; px or CSS string."""
584 minWidth: int | str | None = None
585 """Minimum width; px or CSS string."""
586 minSize: int | str | None = None
587 """Shorthand to set both minWidth and minHeight; px or CSS string."""
588 maxHeight: int | str | None = None
589 """Maximum height; px or CSS string."""
590 maxWidth: int | str | None = None
591 """Maximum width; px or CSS string."""
592 maxSize: int | str | None = None
593 """Shorthand to set both maxWidth and maxHeight; px or CSS string."""
594 margin: int | str | Spacing | None = None
595 """Outer margin; spacing unit, CSS string, or margin object."""
596 background: str | ThemeColor | None = None
597 """Background color; accepts background color token, a primitive color token, a CSS string, or theme-aware `{ light, dark }`.
598
599 Valid tokens: `surface` `surface-secondary` `surface-tertiary` `surface-elevated` `surface-elevated-secondary`
600
601 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
602 """
603 aspectRatio: float | str | None = None
604 """Aspect ratio of the box (e.g., 16/9); number or CSS string."""
605 flex: int | str | None = None
606 """Flex growth/shrink factor."""
607
608
609@_direct_usage_of_named_widget_types_deprecated
610class Button(WidgetComponentBase):
611 """Button component optionally wired to an action."""
612
613 type: Literal["Button"] = Field(default="Button", frozen=True) # pyright: ignore
614 submit: bool | None = None
615 """Configure the button as a submit button for the nearest form."""
616 label: str | None = None
617 """Text to display inside the button."""
618 onClickAction: ActionConfig | None = None
619 """Action dispatched on click."""
620 iconStart: WidgetIcon | None = None
621 """Icon shown before the label; can be used for icon-only buttons."""
622 iconEnd: WidgetIcon | None = None
623 """Optional icon shown after the label."""
624 style: Literal["primary", "secondary"] | None = None
625 """Convenience preset for button style."""
626 iconSize: Literal["sm", "md", "lg", "xl", "2xl"] | None = None
627 """Controls the size of icons within the button; accepts an icon size token."""
628 color: (
629 Literal[
630 "primary",
631 "secondary",
632 "info",
633 "discovery",
634 "success",
635 "caution",
636 "warning",
637 "danger",
638 ]
639 | None
640 ) = None
641 """Color of the button; accepts a button color token."""
642 variant: ControlVariant | None = None
643 """Visual variant of the button; accepts a control variant token."""
644 size: ControlSize | None = None
645 """Controls the overall size of the button."""
646 pill: bool | None = None
647 """Determines if the button should be fully rounded (pill)."""
648 uniform: bool | None = None
649 """Determines if the button should have matching width and height."""
650 block: bool | None = None
651 """Extend the button to 100% of the available width."""
652 disabled: bool | None = None
653 """Disable interactions and apply disabled styles."""
654
655
656@_direct_usage_of_named_widget_types_deprecated
657class Spacer(WidgetComponentBase):
658 """Flexible spacer used to push content apart."""
659
660 type: Literal["Spacer"] = Field(default="Spacer", frozen=True) # pyright: ignore
661 minSize: int | str | None = None
662 """Minimum size the spacer should occupy along the flex direction."""
663
664
665@_direct_usage_of_named_widget_types_deprecated
666class SelectOption(TypedDict):
667 """Selectable option used by the ``Select`` widget."""
668
669 value: str
670 """Option value submitted with the form."""
671 label: str
672 """Human-readable label for the option."""
673 disabled: NotRequired[bool]
674 """Disable the option."""
675 description: NotRequired[str]
676 """Displayed as secondary text below the option `label`."""
677
678
679@_direct_usage_of_named_widget_types_deprecated
680class Select(WidgetComponentBase):
681 """Select dropdown component."""
682
683 type: Literal["Select"] = Field(default="Select", frozen=True) # pyright: ignore
684 name: str
685 """The name of the form control field used when submitting forms."""
686 options: list[SelectOption]
687 """List of selectable options."""
688 onChangeAction: ActionConfig | None = None
689 """Action dispatched when the value changes."""
690 placeholder: str | None = None
691 """Placeholder text shown when no value is selected."""
692 defaultValue: str | None = None
693 """Initial value of the select."""
694 variant: ControlVariant | None = None
695 """Visual style of the select; accepts a control variant token."""
696 size: ControlSize | None = None
697 """Controls the size of the select control."""
698 pill: bool | None = None
699 """Determines if the select should be fully rounded (pill)."""
700 block: bool | None = None
701 """Extend the select to 100% of the available width."""
702 clearable: bool | None = None
703 """Show a clear control to unset the value."""
704 disabled: bool | None = None
705 """Disable interactions and apply disabled styles."""
706
707
708@_direct_usage_of_named_widget_types_deprecated
709class DatePicker(WidgetComponentBase):
710 """Date picker input component."""
711
712 type: Literal["DatePicker"] = Field(default="DatePicker", frozen=True) # pyright: ignore
713 name: str
714 """The name of the form control field used when submitting forms."""
715 onChangeAction: ActionConfig | None = None
716 """Action dispatched when the date value changes."""
717 placeholder: str | None = None
718 """Placeholder text shown when no date is selected."""
719 defaultValue: datetime | None = None
720 """Initial value of the date picker."""
721 min: datetime | None = None
722 """Earliest selectable date (inclusive)."""
723 max: datetime | None = None
724 """Latest selectable date (inclusive)."""
725 variant: ControlVariant | None = None
726 """Visual variant of the datepicker control."""
727 size: ControlSize | None = None
728 """Controls the size of the datepicker control."""
729 side: Literal["top", "bottom", "left", "right"] | None = None
730 """Preferred side to render the calendar."""
731 align: Literal["start", "center", "end"] | None = None
732 """Preferred alignment of the calendar relative to the control."""
733 pill: bool | None = None
734 """Determines if the datepicker should be fully rounded (pill)."""
735 block: bool | None = None
736 """Extend the datepicker to 100% of the available width."""
737 clearable: bool | None = None
738 """Show a clear control to unset the value."""
739 disabled: bool | None = None
740 """Disable interactions and apply disabled styles."""
741
742
743@_direct_usage_of_named_widget_types_deprecated
744class Checkbox(WidgetComponentBase):
745 """Checkbox input component."""
746
747 type: Literal["Checkbox"] = Field(default="Checkbox", frozen=True) # pyright: ignore
748 name: str
749 """The name of the form control field used when submitting forms."""
750 label: str | None = None
751 """Optional label text rendered next to the checkbox."""
752 defaultChecked: bool | None = None
753 """The initial checked state of the checkbox."""
754 onChangeAction: ActionConfig | None = None
755 """Action dispatched when the checked state changes."""
756 disabled: bool | None = None
757 """Disable interactions and apply disabled styles."""
758 required: bool | None = None
759 """Mark the checkbox as required for form submission."""
760
761
762@_direct_usage_of_named_widget_types_deprecated
763class Input(WidgetComponentBase):
764 """Single-line text input component."""
765
766 type: Literal["Input"] = Field(default="Input", frozen=True) # pyright: ignore
767 name: str
768 """The name of the form control field used when submitting forms."""
769 inputType: Literal["number", "email", "text", "password", "tel", "url"] | None = (
770 None
771 )
772 """Native input type."""
773 defaultValue: str | None = None
774 """Initial value of the input."""
775 required: bool | None = None
776 """Mark the input as required for form submission."""
777 pattern: str | None = None
778 """Regex pattern for input validation."""
779 placeholder: str | None = None
780 """Placeholder text shown when empty."""
781 allowAutofillExtensions: bool | None = None
782 """Allow password managers / autofill extensions to appear."""
783 autoSelect: bool | None = None
784 """Select all contents of the input when it mounts."""
785 autoFocus: bool | None = None
786 """Autofocus the input when it mounts."""
787 disabled: bool | None = None
788 """Disable interactions and apply disabled styles."""
789 variant: Literal["soft", "outline"] | None = None
790 """Visual style of the input."""
791 size: ControlSize | None = None
792 """Controls the size of the input control."""
793 gutterSize: Literal["2xs", "xs", "sm", "md", "lg", "xl"] | None = None
794 """Controls gutter on the edges of the input; overrides value from `size`."""
795 pill: bool | None = None
796 """Determines if the input should be fully rounded (pill)."""
797
798
799@_direct_usage_of_named_widget_types_deprecated
800class Label(WidgetComponentBase):
801 """Form label associated with a field."""
802
803 type: Literal["Label"] = Field(default="Label", frozen=True) # pyright: ignore
804 value: str
805 """Text content of the label."""
806 fieldName: str
807 """Name of the field this label describes."""
808 size: TextSize | None = None
809 """Size of the label text; accepts a text size token."""
810 weight: Literal["normal", "medium", "semibold", "bold"] | None = None
811 """Font weight; accepts a font weight token."""
812 textAlign: TextAlign | None = None
813 """Horizontal text alignment."""
814 color: str | ThemeColor | None = None
815 """
816 Text color; accepts a text color token, a primitive color token, a CSS color string, or a theme-aware `{ light, dark }`.
817
818 Text color tokens: `prose` `primary` `emphasis` `secondary` `tertiary` `success` `warning` `danger`
819
820 Primitive color token: e.g. `red-100`, `blue-900`, `gray-500`
821 """
822
823
824@_direct_usage_of_named_widget_types_deprecated
825class RadioOption(TypedDict):
826 """Option inside a ``RadioGroup`` widget."""
827
828 label: str
829 """Label displayed next to the radio option."""
830 value: str
831 """Value submitted when the radio option is selected."""
832 disabled: NotRequired[bool]
833 """Disables a specific radio option."""
834
835
836@_direct_usage_of_named_widget_types_deprecated
837class RadioGroup(WidgetComponentBase):
838 """Grouped radio input control."""
839
840 type: Literal["RadioGroup"] = Field(default="RadioGroup", frozen=True) # pyright: ignore
841 name: str
842 """The name of the form control field used when submitting forms."""
843 options: list[RadioOption] | None = None
844 """Array of options to render as radio items."""
845 ariaLabel: str | None = None
846 """Accessible label for the radio group; falls back to `name`."""
847 onChangeAction: ActionConfig | None = None
848 """Action dispatched when the selected value changes."""
849 defaultValue: str | None = None
850 """Initial selected value of the radio group."""
851 direction: Literal["row", "col"] | None = None
852 """Layout direction of the radio items."""
853 disabled: bool | None = None
854 """Disable interactions and apply disabled styles for the entire group."""
855 required: bool | None = None
856 """Mark the group as required for form submission."""
857
858
859@_direct_usage_of_named_widget_types_deprecated
860class Textarea(WidgetComponentBase):
861 """Multiline text input component."""
862
863 type: Literal["Textarea"] = Field(default="Textarea", frozen=True) # pyright: ignore
864 name: str
865 """The name of the form control field used when submitting forms."""
866 defaultValue: str | None = None
867 """Initial value of the textarea."""
868 required: bool | None = None
869 """Mark the textarea as required for form submission."""
870 pattern: str | None = None
871 """Regex pattern for input validation."""
872 placeholder: str | None = None
873 """Placeholder text shown when empty."""
874 autoSelect: bool | None = None
875 """Select all contents of the textarea when it mounts."""
876 autoFocus: bool | None = None
877 """Autofocus the textarea when it mounts."""
878 disabled: bool | None = None
879 """Disable interactions and apply disabled styles."""
880 variant: Literal["soft", "outline"] | None = None
881 """Visual style of the textarea."""
882 size: ControlSize | None = None
883 """Controls the size of the textarea control."""
884 gutterSize: Literal["2xs", "xs", "sm", "md", "lg", "xl"] | None = None
885 """Controls gutter on the edges of the textarea; overrides value from `size`."""
886 rows: int | None = None
887 """Initial number of visible rows."""
888 autoResize: bool | None = None
889 """Automatically grow/shrink to fit content."""
890 maxRows: int | None = None
891 """Maximum number of rows when auto-resizing."""
892 allowAutofillExtensions: bool | None = None
893 """Allow password managers / autofill extensions to appear."""
894
895
896@_direct_usage_of_named_widget_types_deprecated
897class Transition(WidgetComponentBase):
898 """Wrapper enabling transitions for a child component."""
899
900 type: Literal["Transition"] = Field(default="Transition", frozen=True) # pyright: ignore
901 children: WidgetComponent | None
902 """The child component to animate layout changes for."""
903
904
905@_direct_usage_of_named_widget_types_deprecated
906class Chart(WidgetComponentBase):
907 """Data visualization component for simple bar/line/area charts."""
908
909 type: Literal["Chart"] = Field(default="Chart", frozen=True) # pyright: ignore
910 data: list[dict[str, str | int | float]]
911 """Tabular data for the chart, where each row maps field names to values."""
912 series: list[Series]
913 """One or more series definitions that describe how to visualize data fields."""
914 xAxis: str | XAxisConfig
915 """X-axis configuration; either a `dataKey` string or a config object."""
916 showYAxis: bool | None = None
917 """Controls whether the Y axis is rendered."""
918 showLegend: bool | None = None
919 """Controls whether a legend is rendered."""
920 showTooltip: bool | None = None
921 """Controls whether a tooltip is rendered when hovering over a datapoint."""
922 barGap: int | None = None
923 """Gap between bars within the same category (in px)."""
924 barCategoryGap: int | None = None
925 """Gap between bar categories/groups (in px)."""
926 flex: int | str | None = None
927 """Flex growth/shrink factor for layout."""
928 height: int | str | None = None
929 """Explicit height; px or CSS string."""
930 width: int | str | None = None
931 """Explicit width; px or CSS string."""
932 size: int | str | None = None
933 """Shorthand to set both width and height; px or CSS string."""
934 minHeight: int | str | None = None
935 """Minimum height; px or CSS string."""
936 minWidth: int | str | None = None
937 """Minimum width; px or CSS string."""
938 minSize: int | str | None = None
939 """Shorthand to set both minWidth and minHeight; px or CSS string."""
940 maxHeight: int | str | None = None
941 """Maximum height; px or CSS string."""
942 maxWidth: int | str | None = None
943 """Maximum width; px or CSS string."""
944 maxSize: int | str | None = None
945 """Shorthand to set both maxWidth and maxHeight; px or CSS string."""
946 aspectRatio: float | str | None = None
947 """Aspect ratio of the chart area (e.g., 16/9); number or CSS string."""
948
949
950@_direct_usage_of_named_widget_types_deprecated
951class XAxisConfig(TypedDict):
952 """Configuration object for the X axis."""
953
954 dataKey: str
955 """Field name from each data row to use for X-axis categories."""
956 hide: NotRequired[bool]
957 """Hide the X axis line, ticks, and labels when true."""
958 labels: NotRequired[dict[str, str]]
959 """Custom mapping of tick values to display labels."""
960
961
962CurveType = Literal[
963 "basis",
964 "basisClosed",
965 "basisOpen",
966 "bumpX",
967 "bumpY",
968 "bump",
969 "linear",
970 "linearClosed",
971 "natural",
972 "monotoneX",
973 "monotoneY",
974 "monotone",
975 "step",
976 "stepBefore",
977 "stepAfter",
978]
979"""Interpolation curve types for `area` and `line` series."""
980
981
982@_direct_usage_of_named_widget_types_deprecated
983class BarSeries(BaseModel):
984 """A bar series plotted from a numeric `dataKey`. Supports stacking."""
985
986 type: Literal["bar"] = Field(default="bar", frozen=True)
987 label: str | None
988 """Legend label for the series."""
989 dataKey: str
990 """Field name from each data row that contains the numeric value."""
991 stack: str | None = None
992 """Optional stack group ID. Series with the same ID stack together."""
993 color: str | ThemeColor | None = None
994 """
995 Color for the series; accepts chart color token, a primitive color token, a CSS string, or theme-aware { light, dark }.
996
997 Chart color tokens: `blue` `purple` `orange` `green` `red` `yellow` `pink`
998
999 Primitive color token, e.g., `red-100`, `blue-900`, `gray-500`
1000
1001 Note: By default, a color will be sequentially assigned from the chart series colors.
1002 """
1003
1004
1005@_direct_usage_of_named_widget_types_deprecated
1006class AreaSeries(BaseModel):
1007 """An area series plotted from a numeric `dataKey`. Supports stacking and curves."""
1008
1009 type: Literal["area"] = Field(default="area", frozen=True)
1010 label: str | None
1011 """Legend label for the series."""
1012 dataKey: str
1013 """Field name from each data row that contains the numeric value."""
1014 stack: str | None = None
1015 """Optional stack group ID. Series with the same ID stack together."""
1016 color: str | ThemeColor | None = None
1017 """
1018 Color for the series; accepts chart color token, a primitive color token, a CSS string, or theme-aware { light, dark }.
1019
1020 Chart color tokens: `blue` `purple` `orange` `green` `red` `yellow` `pink`
1021
1022 Primitive color token, e.g., `red-100`, `blue-900`, `gray-500`
1023
1024 Note: By default, a color will be sequentially assigned from the chart series colors.
1025 """
1026 curveType: None | Literal[CurveType] = None
1027 """Interpolation curve type used to connect points."""
1028
1029
1030@_direct_usage_of_named_widget_types_deprecated
1031class LineSeries(BaseModel):
1032 """A line series plotted from a numeric `dataKey`. Supports curves."""
1033
1034 type: Literal["line"] = Field(default="line", frozen=True)
1035 label: str | None
1036 """Legend label for the series."""
1037 dataKey: str
1038 """Field name from each data row that contains the numeric value."""
1039 color: str | ThemeColor | None = None
1040 """
1041 Color for the series; accepts chart color token, a primitive color token, a CSS string, or theme-aware { light, dark }.
1042
1043 Chart color tokens: `blue` `purple` `orange` `green` `red` `yellow` `pink`
1044
1045 Primitive color token, e.g., `red-100`, `blue-900`, `gray-500`
1046
1047 Note: By default, a color will be sequentially assigned from the chart series colors.
1048 """
1049 curveType: None | Literal[CurveType] = None
1050 """Interpolation curve type used to connect points."""
1051
1052
1053Series = Annotated[
1054 BarSeries | AreaSeries | LineSeries,
1055 Field(discriminator="type"),
1056]
1057"""Union of all supported chart series types."""
1058
1059
1060class DynamicWidgetComponent(WidgetComponentBase):
1061 """
1062 A widget component with a statically defined base shape but dynamically
1063 defined additional fields loaded from a widget template or JSON schema.
1064 """
1065
1066 model_config = ConfigDict(extra="allow")
1067 children: DynamicWidgetComponent | list[DynamicWidgetComponent] | None = None
1068
1069
1070StrictWidgetComponent = Annotated[
1071 Text
1072 | Title
1073 | Caption
1074 | Chart
1075 | Badge
1076 | Markdown
1077 | Box
1078 | Row
1079 | Col
1080 | Divider
1081 | Icon
1082 | Image
1083 | ListViewItem
1084 | Button
1085 | Checkbox
1086 | Spacer
1087 | Select
1088 | DatePicker
1089 | Form
1090 | Input
1091 | Label
1092 | RadioGroup
1093 | Textarea
1094 | Transition,
1095 Field(discriminator="type"),
1096]
1097
1098
1099StrictWidgetRoot = Annotated[
1100 Card | ListView,
1101 Field(discriminator="type"),
1102]
1103
1104
1105class DynamicWidgetRoot(DynamicWidgetComponent):
1106 """Dynamic root widget restricted to root types."""
1107
1108 type: Literal["Card", "ListView"] # pyright: ignore
1109
1110
1111class BasicRoot(DynamicWidgetComponent):
1112 """Layout root capable of nesting components or other roots."""
1113
1114 type: Literal["Basic"] = Field(default="Basic", frozen=True) # pyright: ignore
1115
1116
1117WidgetComponent = StrictWidgetComponent | DynamicWidgetComponent
1118"""Union of all renderable widget components."""
1119
1120WidgetRoot = StrictWidgetRoot | DynamicWidgetRoot
1121"""Union of all renderable top-level widgets."""
1122
1123
1124WidgetIcon = IconName
1125"""Icon names accepted by widgets that render icons."""
1126
1127
1128class WidgetTemplate:
1129 """
1130 Utility for loading and building widgets from a .widget file.
1131
1132 Example using .widget file on disc:
1133 ```python
1134 template = WidgetTemplate.from_file("path/to/my_widget.widget")
1135 widget = template.build({"name": "Harry Potter"})
1136 ```
1137
1138 Example using already parsed widget definition:
1139 ```python
1140 template = WidgetTemplate(definition={"version": "1.0", "name": "...", "template": Template(...), "jsonSchema": {...}})
1141 widget = template.build({"name": "Harry Potter"})
1142 ```
1143 """
1144
1145 def __init__(self, definition: dict[str, Any]):
1146 self.version = definition["version"]
1147 if self.version != "1.0":
1148 raise ValueError(f"Unsupported widget spec version: {self.version}")
1149
1150 self.name = definition["name"]
1151 template = definition["template"]
1152 if isinstance(template, Template):
1153 self.template = template
1154 else:
1155 self.template = _jinja_env.from_string(template)
1156 self.data_schema = definition.get("jsonSchema", {})
1157
1158 @classmethod
1159 def from_file(cls, file_path: str) -> WidgetTemplate:
1160 path = Path(file_path)
1161 if not path.is_absolute():
1162 caller_frame = inspect.stack()[1]
1163 caller_path = Path(caller_frame.filename).resolve()
1164 path = caller_path.parent / path
1165
1166 with path.open("r", encoding="utf-8") as file:
1167 payload = json.load(file)
1168
1169 return cls(payload)
1170
1171 def build(
1172 self, data: dict[str, Any] | BaseModel | None = None
1173 ) -> DynamicWidgetRoot:
1174 """Render the widget template with the given data and return a DynamicWidgetRoot instance."""
1175 rendered = self.template.render(**self._normalize_data(data))
1176 widget_dict = json.loads(rendered)
1177 return DynamicWidgetRoot.model_validate(widget_dict)
1178
1179 def build_basic(self, data: dict[str, Any] | BaseModel | None = None) -> BasicRoot:
1180 """Separate method for building basic root widgets until BasicRoot is supported for streamed widgets."""
1181 rendered = self.template.render(**self._normalize_data(data))
1182 widget_dict = json.loads(rendered)
1183 return BasicRoot.model_validate(widget_dict)
1184
1185 def _normalize_data(
1186 self, data: dict[str, Any] | BaseModel | None
1187 ) -> dict[str, Any]:
1188 if data is None:
1189 return {}
1190 return data.model_dump() if isinstance(data, BaseModel) else data
1191