openai/chatkit-python

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.6.4

Branches

Tags

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

Clone

HTTPS

Download ZIP

chatkit/widgets.py

1193lines · modeblame

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