From 91c93ee62c41670edcae64e1d046f3bcc6b25563 Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Fri, 21 Nov 2025 12:00:18 -0800 Subject: [PATCH 1/4] Add WidgetTemplate and DynamicWidgetRoot/Component types --- chatkit/server.py | 28 ++-- chatkit/widgets.py | 136 +++++++++++++--- pyproject.toml | 2 +- tests/assets/widgets/card_no_data.json | 71 ++++++++ tests/assets/widgets/card_no_data.widget | 83 ++++++++++ tests/assets/widgets/card_with_data.json | 56 +++++++ tests/assets/widgets/card_with_data.widget | 96 +++++++++++ tests/assets/widgets/list_view_no_data.json | 53 ++++++ tests/assets/widgets/list_view_no_data.widget | 65 ++++++++ tests/assets/widgets/list_view_with_data.json | 77 +++++++++ .../assets/widgets/list_view_with_data.widget | 121 ++++++++++++++ tests/test_widgets.py | 154 +++++++++++++++++- uv.lock | 2 + 13 files changed, 910 insertions(+), 34 deletions(-) create mode 100644 tests/assets/widgets/card_no_data.json create mode 100644 tests/assets/widgets/card_no_data.widget create mode 100644 tests/assets/widgets/card_with_data.json create mode 100644 tests/assets/widgets/card_with_data.widget create mode 100644 tests/assets/widgets/list_view_no_data.json create mode 100644 tests/assets/widgets/list_view_no_data.widget create mode 100644 tests/assets/widgets/list_view_with_data.json create mode 100644 tests/assets/widgets/list_view_with_data.widget diff --git a/chatkit/server.py b/chatkit/server.py index 66d7b58..970c973 100644 --- a/chatkit/server.py +++ b/chatkit/server.py @@ -69,7 +69,7 @@ is_streaming_req, ) from .version import __version__ -from .widgets import Markdown, Text, WidgetComponent, WidgetComponentBase, WidgetRoot +from .widgets import WidgetComponent, WidgetComponentBase, WidgetRoot DEFAULT_PAGE_SIZE = 20 DEFAULT_ERROR_MESSAGE = "An error occurred when generating a response." @@ -82,6 +82,11 @@ def diff_widget( Compare two WidgetRoots and return a list of deltas. """ + def is_streaming_text(component: WidgetComponentBase) -> bool: + return getattr(component, "type", None) in {"Markdown", "Text"} and isinstance( + getattr(component, "value", None), str + ) + def full_replace(before: WidgetComponentBase, after: WidgetComponentBase) -> bool: if ( before.type != after.type @@ -108,10 +113,10 @@ def full_replace_value(before_value: Any, after_value: Any) -> bool: for field in before.model_fields_set.union(after.model_fields_set): if ( - isinstance(before, (Markdown, Text)) - and isinstance(after, (Markdown, Text)) + is_streaming_text(before) + and is_streaming_text(after) and field == "value" - and after.value.startswith(before.value) + and getattr(after, "value", "").startswith(getattr(before, "value", "")) ): # Appends to the value prop of Markdown or Text do not trigger a full replace continue @@ -129,11 +134,11 @@ def full_replace_value(before_value: Any, after_value: Any) -> bool: def find_all_streaming_text_components( component: WidgetComponent | WidgetRoot, - ) -> dict[str, Markdown | Text]: + ) -> dict[str, WidgetComponentBase]: components = {} def recurse(component: WidgetComponent | WidgetRoot): - if isinstance(component, (Markdown, Text)) and component.id: + if is_streaming_text(component) and component.id: components[component.id] = component if hasattr(component, "children"): @@ -154,16 +159,19 @@ def recurse(component: WidgetComponent | WidgetRoot): f"Node {id} was not present when the widget was initially rendered. All nodes with ID must persist across all widget updates." ) - if before_node.value != after_node.value: - if not after_node.value.startswith(before_node.value): + before_value = str(getattr(before_node, "value", None)) + after_value = str(getattr(after_node, "value", None)) + + if before_value != after_value: + if not after_value.startswith(before_value): raise ValueError( f"Node {id} was updated with a new value that is not a prefix of the initial value. All widget updates must be cumulative." ) - done = not after_node.streaming + done = not getattr(after_node, "streaming", False) deltas.append( WidgetStreamingTextValueDelta( component_id=id, - delta=after_node.value[len(before_node.value) :], + delta=after_value[len(before_value) :], done=done, ) ) diff --git a/chatkit/widgets.py b/chatkit/widgets.py index eb64341..09083a5 100644 --- a/chatkit/widgets.py +++ b/chatkit/widgets.py @@ -1,15 +1,21 @@ from __future__ import annotations +import inspect +import json from datetime import datetime +from pathlib import Path from typing import ( Annotated, + Any, Literal, ) +from jinja2 import Environment, StrictUndefined, Template from pydantic import ( BaseModel, ConfigDict, Field, + TypeAdapter, model_serializer, ) from typing_extensions import NotRequired, TypedDict @@ -17,6 +23,8 @@ from .actions import ActionConfig from .icons import IconName +env = Environment(undefined=StrictUndefined) + class ThemeColor(TypedDict): """Color values for light and dark themes.""" @@ -1006,32 +1014,23 @@ class LineSeries(BaseModel): """Union of all supported chart series types.""" -class BasicRoot(WidgetComponentBase): - """Layout root capable of nesting components or other roots.""" - - type: Literal["Basic"] = Field(default="Basic", frozen=True) # pyright: ignore - children: list[WidgetComponent | WidgetRoot] - """Children to render inside this root. Can include widget components or nested roots.""" - theme: Literal["light", "dark"] | None = None - """Force light or dark theme for this subtree.""" - direction: Literal["row", "col"] | None = None - """Flex direction for laying out direct children.""" - gap: int | str | None = None - """Gap between direct children; spacing unit or CSS string.""" - padding: float | str | Spacing | None = None - """Inner padding; spacing unit, CSS string, or padding object.""" - align: Alignment | None = None - """Cross-axis alignment of children.""" - justify: Justification | None = None - """Main-axis distribution of children.""" - - WidgetRoot = Annotated[ Card | ListView, Field(discriminator="type"), ] -WidgetComponent = Annotated[ + +class DynamicWidgetComponent(WidgetComponentBase): + """ + A widget component with a statically defined base shape but dynamically + defined additional fields loaded from a widget template or JSON schema. + """ + + model_config = ConfigDict(extra="allow") + children: list["DynamicWidgetComponent"] | None = None + + +StrictWidgetComponent = Annotated[ Text | Title | Caption @@ -1058,8 +1057,103 @@ class BasicRoot(WidgetComponentBase): | Transition, Field(discriminator="type"), ] + + +StrictWidgetRoot = Annotated[ + Card | ListView, + Field(discriminator="type"), +] + + +class DynamicWidgetRoot(DynamicWidgetComponent): + """Dynamic root widget restricted to root types.""" + + type: Literal["Card", "ListView"] # pyright: ignore + + +class BasicRoot(WidgetComponentBase): + """Layout root capable of nesting components or other roots.""" + + type: Literal["Basic"] = Field(default="Basic", frozen=True) # pyright: ignore + children: list[WidgetComponent | WidgetRoot] + """Children to render inside this root. Can include widget components or nested roots.""" + theme: Literal["light", "dark"] | None = None + """Force light or dark theme for this subtree.""" + direction: Literal["row", "col"] | None = None + """Flex direction for laying out direct children.""" + gap: int | str | None = None + """Gap between direct children; spacing unit or CSS string.""" + padding: float | str | Spacing | None = None + """Inner padding; spacing unit, CSS string, or padding object.""" + align: Alignment | None = None + """Cross-axis alignment of children.""" + justify: Justification | None = None + """Main-axis distribution of children.""" + + +WidgetComponent = StrictWidgetComponent | DynamicWidgetComponent """Union of all renderable widget components.""" +WidgetRoot = StrictWidgetRoot | DynamicWidgetRoot +"""Union of all renderable top-level widgets.""" + WidgetIcon = IconName """Icon names accepted by widgets that render icons.""" + + +class WidgetTemplate: + """ + Utility for loading and building widgets from a .widget file. + + Example using .widget file on disc: + ```python + template = WidgetTemplate.from_file("path/to/my_widget.widget") + widget = template.build({"name": "Harry Potter"}) + ``` + + Example using already parsed widget definition: + ```python + template = WidgetTemplate(definition={"version": "1.0", "name": "...", "template": Template(...), "jsonSchema": {...}}) + widget = template.build({"name": "Harry Potter"}) + ``` + """ + + adapter: TypeAdapter[DynamicWidgetRoot] = TypeAdapter(DynamicWidgetRoot) + + def __init__(self, definition: dict[str, Any]): + self.version = definition["version"] + if self.version != "1.0": + raise ValueError(f"Unsupported widget spec version: {self.version}") + + self.name = definition["name"] + template = definition["template"] + if isinstance(template, Template): + self.template = template + else: + self.template = env.from_string(template) + self.data_schema = definition.get("jsonSchema", {}) + + @classmethod + def from_file(cls, file_path: str) -> "WidgetTemplate": + path = Path(file_path) + if not path.is_absolute(): + caller_frame = inspect.stack()[1] + caller_path = Path(caller_frame.filename).resolve() + path = caller_path.parent / path + + with path.open("r", encoding="utf-8") as file: + payload = json.load(file) + + return cls(payload) + + def build( + self, data: dict[str, Any] | BaseModel | None = None + ) -> DynamicWidgetRoot: + if data is None: + data = {} + if isinstance(data, BaseModel): + data = data.model_dump() + rendered = self.template.render(**data) + widget_dict = json.loads(rendered) + return self.adapter.validate_python(widget_dict) diff --git a/pyproject.toml b/pyproject.toml index 10a7100..a80fb01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "1.3.1" description = "A ChatKit backend SDK." readme = "README.md" requires-python = ">=3.10" -dependencies = ["pydantic", "uvicorn", "openai", "openai-agents>=0.3.2"] +dependencies = ["pydantic", "uvicorn", "openai", "openai-agents>=0.3.2", "jinja2"] [dependency-groups] dev = [ diff --git a/tests/assets/widgets/card_no_data.json b/tests/assets/widgets/card_no_data.json new file mode 100644 index 0000000..995c11f --- /dev/null +++ b/tests/assets/widgets/card_no_data.json @@ -0,0 +1,71 @@ +{ + "type": "Card", + "children": [ + { + "type": "Col", + "align": "center", + "gap": 4, + "padding": 4, + "children": [ + { + "type": "Box", + "background": "green-400", + "radius": "full", + "padding": 3, + "children": [ + { + "type": "Icon", + "name": "check", + "size": "3xl", + "color": "white" + } + ] + }, + { + "type": "Col", + "align": "center", + "gap": 1, + "children": [ + { + "type": "Title", + "value": "Enable notification" + }, + { + "type": "Text", + "value": "Notify me when this item ships", + "color": "secondary" + } + ] + } + ] + }, + { + "type": "Row", + "children": [ + { + "type": "Button", + "label": "Yes", + "block": true, + "onClickAction": { + "type": "notification.settings", + "payload": { + "enable": true + } + } + }, + { + "type": "Button", + "label": "No", + "block": true, + "variant": "outline", + "onClickAction": { + "type": "notification.settings", + "payload": { + "enable": true + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/assets/widgets/card_no_data.widget b/tests/assets/widgets/card_no_data.widget new file mode 100644 index 0000000..a4974e7 --- /dev/null +++ b/tests/assets/widgets/card_no_data.widget @@ -0,0 +1,83 @@ +{ + "version": "1.0", + "name": "Enable Notification", + "template": "{\"type\":\"Card\",\"children\":[{\"type\":\"Col\",\"align\":\"center\",\"gap\":4,\"padding\":4,\"children\":[{\"type\":\"Box\",\"background\":\"green-400\",\"radius\":\"full\",\"padding\":3,\"children\":[{\"type\":\"Icon\",\"name\":\"check\",\"size\":\"3xl\",\"color\":\"white\"}]},{\"type\":\"Col\",\"align\":\"center\",\"gap\":1,\"children\":[{\"type\":\"Title\",\"value\":\"Enable notification\"},{\"type\":\"Text\",\"value\":\"Notify me when this item ships\",\"color\":\"secondary\"}]}]},{\"type\":\"Row\",\"children\":[{\"type\":\"Button\",\"label\":\"Yes\",\"block\":true,\"onClickAction\":{\"type\":\"notification.settings\",\"payload\":{\"enable\":true}}},{\"type\":\"Button\",\"label\":\"No\",\"block\":true,\"variant\":\"outline\",\"onClickAction\":{\"type\":\"notification.settings\",\"payload\":{\"enable\":true}}}]}]}", + "jsonSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "outputJsonPreview": { + "type": "Card", + "children": [ + { + "type": "Col", + "align": "center", + "gap": 4, + "padding": 4, + "children": [ + { + "type": "Box", + "background": "green-400", + "radius": "full", + "padding": 3, + "children": [ + { + "type": "Icon", + "name": "check", + "size": "3xl", + "color": "white" + } + ] + }, + { + "type": "Col", + "align": "center", + "gap": 1, + "children": [ + { + "type": "Title", + "value": "Enable notification" + }, + { + "type": "Text", + "value": "Notify me when this item ships", + "color": "secondary" + } + ] + } + ] + }, + { + "type": "Row", + "children": [ + { + "type": "Button", + "label": "Yes", + "block": true, + "onClickAction": { + "type": "notification.settings", + "payload": { + "enable": true + } + } + }, + { + "type": "Button", + "label": "No", + "block": true, + "variant": "outline", + "onClickAction": { + "type": "notification.settings", + "payload": { + "enable": true + } + } + } + ] + } + ] + }, + "encodedWidget": "eyJpZCI6IndpZ180YWtnYWg0YSIsIm5hbWUiOiJFbmFibGUgTm90aWZpY2F0aW9uIiwidmlldyI6IjxDYXJkPlxuICA8Q29sIGFsaWduPVwiY2VudGVyXCIgZ2FwPXs0fSBwYWRkaW5nPXs0fT5cbiAgICA8Qm94IGJhY2tncm91bmQ9XCJncmVlbi00MDBcIiByYWRpdXM9XCJmdWxsXCIgcGFkZGluZz17M30-XG4gICAgICA8SWNvbiBuYW1lPVwiY2hlY2tcIiBzaXplPVwiM3hsXCIgY29sb3I9XCJ3aGl0ZVwiIC8-XG4gICAgPC9Cb3g-XG4gICAgPENvbCBhbGlnbj1cImNlbnRlclwiIGdhcD17MX0-XG4gICAgICA8VGl0bGUgdmFsdWU9XCJFbmFibGUgbm90aWZpY2F0aW9uXCIgLz5cbiAgICAgIDxUZXh0IHZhbHVlPVwiTm90aWZ5IG1lIHdoZW4gdGhpcyBpdGVtIHNoaXBzXCIgY29sb3I9XCJzZWNvbmRhcnlcIiAvPlxuICAgIDwvQ29sPlxuICA8L0NvbD5cblxuICA8Um93PlxuICAgIDxCdXR0b25cbiAgICAgIGxhYmVsPVwiWWVzXCJcbiAgICAgIGJsb2NrXG4gICAgICBvbkNsaWNrQWN0aW9uPXt7XG4gICAgICAgIHR5cGU6IFwibm90aWZpY2F0aW9uLnNldHRpbmdzXCIsXG4gICAgICAgIHBheWxvYWQ6IHsgZW5hYmxlOiB0cnVlIH0sXG4gICAgICB9fVxuICAgIC8-XG4gICAgPEJ1dHRvblxuICAgICAgbGFiZWw9XCJOb1wiXG4gICAgICBibG9ja1xuICAgICAgdmFyaWFudD1cIm91dGxpbmVcIlxuICAgICAgb25DbGlja0FjdGlvbj17e1xuICAgICAgICB0eXBlOiBcIm5vdGlmaWNhdGlvbi5zZXR0aW5nc1wiLFxuICAgICAgICBwYXlsb2FkOiB7IGVuYWJsZTogdHJ1ZSB9LFxuICAgICAgfX1cbiAgICAvPlxuICA8L1Jvdz5cbjwvQ2FyZD4iLCJkZWZhdWx0U3RhdGUiOnt9LCJzY2hlbWFNb2RlIjoiem9kIiwianNvblNjaGVtYSI6e30sInNjaGVtYSI6ImltcG9ydCB7IHogfSBmcm9tIFwiem9kXCJcblxuY29uc3QgV2lkZ2V0U3RhdGUgPSB6Lm9iamVjdCh7fSlcblxuZXhwb3J0IGRlZmF1bHQgV2lkZ2V0U3RhdGUiLCJzdGF0ZXMiOltdLCJzY2hlbWFWYWxpZGl0eSI6InZhbGlkIiwidmlld1ZhbGlkaXR5IjoidmFsaWQiLCJkZWZhdWx0U3RhdGVWYWxpZGl0eSI6InZhbGlkIn0" +} \ No newline at end of file diff --git a/tests/assets/widgets/card_with_data.json b/tests/assets/widgets/card_with_data.json new file mode 100644 index 0000000..a1d06f7 --- /dev/null +++ b/tests/assets/widgets/card_with_data.json @@ -0,0 +1,56 @@ +{ + "type": "Card", + "size": "md", + "children": [ + { + "type": "Row", + "children": [ + { + "type": "Text", + "value": "#proj-chatkit" + }, + { + "type": "Spacer" + }, + { + "type": "Text", + "value": "4:48 PM", + "color": "tertiary" + } + ] + }, + { + "type": "Divider", + "flush": true + }, + { + "type": "Row", + "align": "start", + "gap": 4, + "children": [ + { + "type": "Image", + "src": "/pam.png", + "size": 44 + }, + { + "type": "Col", + "children": [ + { + "type": "Text", + "value": "Pam Beesly", + "weight": "semibold" + }, + { + "type": "Markdown", + "value": "End of week update for ChatKit:\n\n1. Designed **new header system** with more flexibility for custom menu actions.\n2. Made progress on **DevDay training material**.\n3. Coordinated with partners to **prioritize remaining feature requirements**.\n\n**Next week** I plan to focus on building out our Figma library and updating to new icons." + } + ] + } + ] + }, + { + "type": "Spacer" + } + ] +} \ No newline at end of file diff --git a/tests/assets/widgets/card_with_data.widget b/tests/assets/widgets/card_with_data.widget new file mode 100644 index 0000000..6a07a48 --- /dev/null +++ b/tests/assets/widgets/card_with_data.widget @@ -0,0 +1,96 @@ +{ + "version": "1.0", + "name": "Channel message", + "template": "{\"type\":\"Card\",\"size\":\"md\",\"children\":[{\"type\":\"Row\",\"children\":[{\"type\":\"Text\",\"value\":{{ (channel) | tojson }}},{\"type\":\"Spacer\"},{\"type\":\"Text\",\"value\":{{ (time) | tojson }},\"color\":\"tertiary\"}]},{\"type\":\"Divider\",\"flush\":true},{\"type\":\"Row\",\"align\":\"start\",\"gap\":4,\"children\":[{\"type\":\"Image\",\"src\":{{ (user.image) | tojson }},\"size\":44},{\"type\":\"Col\",\"children\":[{\"type\":\"Text\",\"value\":{{ (user.name) | tojson }},\"weight\":\"semibold\"},{\"type\":\"Markdown\",\"value\":{{ ((\"End of week update for ChatKit:\\n\\n1. Designed **new header system** with more flexibility for custom menu actions.\\n2. Made progress on **DevDay training material**.\\n3. Coordinated with partners to **prioritize remaining feature requirements**.\\n\\n**Next week** I plan to focus on building out our Figma library and updating to new icons.\")) | tojson }}}]}]},{\"type\":\"Spacer\"}]}", + "jsonSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "channel": { + "type": "string" + }, + "time": { + "type": "string" + }, + "user": { + "type": "object", + "properties": { + "image": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "image", + "name" + ], + "additionalProperties": false + } + }, + "required": [ + "channel", + "time", + "user" + ], + "additionalProperties": false + }, + "outputJsonPreview": { + "type": "Card", + "size": "md", + "children": [ + { + "type": "Row", + "children": [ + { + "type": "Text", + "value": "#proj-chatkit" + }, + { + "type": "Spacer" + }, + { + "type": "Text", + "value": "4:48 PM", + "color": "tertiary" + } + ] + }, + { + "type": "Divider", + "flush": true + }, + { + "type": "Row", + "align": "start", + "gap": 4, + "children": [ + { + "type": "Image", + "src": "/zj.png", + "size": 44 + }, + { + "type": "Col", + "children": [ + { + "type": "Text", + "value": "Zach Johnston", + "weight": "semibold" + }, + { + "type": "Markdown", + "value": "End of week update for ChatKit:\n\n1. Designed **new header system** with more flexibility for custom menu actions.\n2. Made progress on **DevDay training material**.\n3. Coordinated with partners to **prioritize remaining feature requirements**.\n\n**Next week** I plan to focus on building out our Figma library and updating to new icons." + } + ] + } + ] + }, + { + "type": "Spacer" + } + ] + }, + "encodedWidget": "eyJpZCI6IndpZ183N3AzNzIxeiIsIm5hbWUiOiJDaGFubmVsIG1lc3NhZ2UiLCJ2aWV3IjoiPENhcmQgc2l6ZT1cIm1kXCI-XG4gIDxSb3c-XG4gICAgPFRleHQgdmFsdWU9e2NoYW5uZWx9IC8-XG4gICAgPFNwYWNlciAvPlxuICAgIDxUZXh0IHZhbHVlPXt0aW1lfSBjb2xvcj1cInRlcnRpYXJ5XCIgLz5cbiAgPC9Sb3c-XG4gIDxEaXZpZGVyIGZsdXNoIC8-XG4gIDxSb3cgYWxpZ249XCJzdGFydFwiIGdhcD17NH0-XG4gICAgPEltYWdlIHNyYz17dXNlci5pbWFnZX0gc2l6ZT17NDR9IC8-XG4gICAgPENvbD5cbiAgICAgIDxUZXh0IHZhbHVlPXt1c2VyLm5hbWV9IHdlaWdodD1cInNlbWlib2xkXCIgLz5cbiAgICAgIDxNYXJrZG93blxuICAgICAgICB2YWx1ZT17YEVuZCBvZiB3ZWVrIHVwZGF0ZSBmb3IgQ2hhdEtpdDpcblxuMS4gRGVzaWduZWQgKipuZXcgaGVhZGVyIHN5c3RlbSoqIHdpdGggbW9yZSBmbGV4aWJpbGl0eSBmb3IgY3VzdG9tIG1lbnUgYWN0aW9ucy5cbjIuIE1hZGUgcHJvZ3Jlc3Mgb24gKipEZXZEYXkgdHJhaW5pbmcgbWF0ZXJpYWwqKi5cbjMuIENvb3JkaW5hdGVkIHdpdGggcGFydG5lcnMgdG8gKipwcmlvcml0aXplIHJlbWFpbmluZyBmZWF0dXJlIHJlcXVpcmVtZW50cyoqLlxuXG4qKk5leHQgd2VlayoqIEkgcGxhbiB0byBmb2N1cyBvbiBidWlsZGluZyBvdXQgb3VyIEZpZ21hIGxpYnJhcnkgYW5kIHVwZGF0aW5nIHRvIG5ldyBpY29ucy5gfVxuICAgICAgLz5cbiAgICA8L0NvbD5cbiAgPC9Sb3c-XG4gIDxTcGFjZXIgLz5cbjwvQ2FyZD4iLCJkZWZhdWx0U3RhdGUiOnsiY2hhbm5lbCI6IiNwcm9qLWNoYXRraXQiLCJ0aW1lIjoiNDo0OCBQTSIsInVzZXIiOnsiaW1hZ2UiOiIvemoucG5nIiwibmFtZSI6IlphY2ggSm9obnN0b24ifX0sInNjaGVtYU1vZGUiOiJ6b2QiLCJqc29uU2NoZW1hIjp7fSwic2NoZW1hIjoiaW1wb3J0IHsgeiB9IGZyb20gXCJ6b2RcIlxuXG5jb25zdCBVc2VyID0gei5vYmplY3Qoe1xuICBpbWFnZTogei5zdHJpbmcoKSxcbiAgbmFtZTogei5zdHJpbmcoKSxcbn0pXG5cbmNvbnN0IFdpZGdldFN0YXRlID0gei5vYmplY3Qoe1xuICBjaGFubmVsOiB6LnN0cmluZygpLFxuICB0aW1lOiB6LnN0cmluZygpLFxuICB1c2VyOiBVc2VyLFxufSlcblxuZXhwb3J0IGRlZmF1bHQgV2lkZ2V0U3RhdGUiLCJzdGF0ZXMiOltdLCJzY2hlbWFWYWxpZGl0eSI6InZhbGlkIiwidmlld1ZhbGlkaXR5IjoidmFsaWQiLCJkZWZhdWx0U3RhdGVWYWxpZGl0eSI6InZhbGlkIn0" +} \ No newline at end of file diff --git a/tests/assets/widgets/list_view_no_data.json b/tests/assets/widgets/list_view_no_data.json new file mode 100644 index 0000000..fea7818 --- /dev/null +++ b/tests/assets/widgets/list_view_no_data.json @@ -0,0 +1,53 @@ +{ + "type": "ListView", + "children": [ + { + "type": "ListViewItem", + "key": "blue", + "gap": 5, + "onClickAction": { + "type": "line.select", + "payload": { + "id": "blue" + } + }, + "children": [ + { + "type": "Box", + "background": "blue-500", + "radius": "full", + "size": 25 + }, + { + "type": "Text", + "value": "Blue line", + "size": "sm" + } + ] + }, + { + "type": "ListViewItem", + "key": "orange", + "gap": 5, + "onClickAction": { + "type": "line.select", + "payload": { + "id": "orange" + } + }, + "children": [ + { + "type": "Box", + "background": "orange-500", + "radius": "full", + "size": 25 + }, + { + "type": "Text", + "value": "Orange line", + "size": "sm" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/assets/widgets/list_view_no_data.widget b/tests/assets/widgets/list_view_no_data.widget new file mode 100644 index 0000000..a6e272c --- /dev/null +++ b/tests/assets/widgets/list_view_no_data.widget @@ -0,0 +1,65 @@ +{ + "version": "1.0", + "name": "Untitled widget", + "template": "{\"type\":\"ListView\",\"children\":[{\"type\":\"ListViewItem\",\"key\":\"blue\",\"gap\":5,\"onClickAction\":{\"type\":\"line.select\",\"payload\":{\"id\":\"blue\"}},\"children\":[{\"type\":\"Box\",\"background\":\"blue-500\",\"radius\":\"full\",\"size\":25},{\"type\":\"Text\",\"value\":\"Blue line\",\"size\":\"sm\"}]},{\"type\":\"ListViewItem\",\"key\":\"orange\",\"gap\":5,\"onClickAction\":{\"type\":\"line.select\",\"payload\":{\"id\":\"orange\"}},\"children\":[{\"type\":\"Box\",\"background\":\"orange-500\",\"radius\":\"full\",\"size\":25},{\"type\":\"Text\",\"value\":\"Orange line\",\"size\":\"sm\"}]}]}", + "jsonSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": {}, + "additionalProperties": false + }, + "outputJsonPreview": { + "type": "ListView", + "children": [ + { + "type": "ListViewItem", + "key": "blue", + "gap": 5, + "onClickAction": { + "type": "line.select", + "payload": { + "id": "blue" + } + }, + "children": [ + { + "type": "Box", + "background": "blue-500", + "radius": "full", + "size": 25 + }, + { + "type": "Text", + "value": "Blue line", + "size": "sm" + } + ] + }, + { + "type": "ListViewItem", + "key": "orange", + "gap": 5, + "onClickAction": { + "type": "line.select", + "payload": { + "id": "orange" + } + }, + "children": [ + { + "type": "Box", + "background": "orange-500", + "radius": "full", + "size": 25 + }, + { + "type": "Text", + "value": "Orange line", + "size": "sm" + } + ] + } + ] + }, + "encodedWidget": "eyJpZCI6IndpZ18xN2U5dWtoaiIsIm5hbWUiOiJVbnRpdGxlZCB3aWRnZXQiLCJ2aWV3IjoiPExpc3RWaWV3PlxuICA8TGlzdFZpZXdJdGVtXG4gICAga2V5PVwiYmx1ZVwiXG4gICAgZ2FwPXs1fVxuICAgIG9uQ2xpY2tBY3Rpb249e3sgdHlwZTogXCJsaW5lLnNlbGVjdFwiLCBwYXlsb2FkOiB7IGlkOiBcImJsdWVcIiB9IH19XG4gID5cbiAgICA8Qm94IGJhY2tncm91bmQ9XCJibHVlLTUwMFwiIHJhZGl1cz1cImZ1bGxcIiBzaXplPXsyNX0gLz5cbiAgICA8VGV4dCB2YWx1ZT1cIkJsdWUgbGluZVwiIHNpemU9XCJzbVwiIC8-XG4gIDwvTGlzdFZpZXdJdGVtPlxuICA8TGlzdFZpZXdJdGVtXG4gICAga2V5PVwib3JhbmdlXCJcbiAgICBnYXA9ezV9XG4gICAgb25DbGlja0FjdGlvbj17eyB0eXBlOiBcImxpbmUuc2VsZWN0XCIsIHBheWxvYWQ6IHsgaWQ6IFwib3JhbmdlXCIgfSB9fVxuICA-XG4gICAgPEJveCBiYWNrZ3JvdW5kPVwib3JhbmdlLTUwMFwiIHJhZGl1cz1cImZ1bGxcIiBzaXplPXsyNX0gLz5cbiAgICA8VGV4dCB2YWx1ZT1cIk9yYW5nZSBsaW5lXCIgc2l6ZT1cInNtXCIgLz5cbiAgPC9MaXN0Vmlld0l0ZW0-XG48L0xpc3RWaWV3PiIsImRlZmF1bHRTdGF0ZSI6e30sInNjaGVtYU1vZGUiOiJ6b2QiLCJqc29uU2NoZW1hIjp7InR5cGUiOiJvYmplY3QiLCJwcm9wZXJ0aWVzIjp7InRpdGxlIjp7InR5cGUiOiJzdHJpbmcifX0sInJlcXVpcmVkIjpbInRpdGxlIl0sImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZX0sInNjaGVtYSI6ImltcG9ydCB7IHogfSBmcm9tIFwiem9kXCJcblxuY29uc3QgV2lkZ2V0U3RhdGUgPSB6LnN0cmljdE9iamVjdCh7fSlcblxuZXhwb3J0IGRlZmF1bHQgV2lkZ2V0U3RhdGUiLCJzdGF0ZXMiOltdLCJzY2hlbWFWYWxpZGl0eSI6InZhbGlkIiwidmlld1ZhbGlkaXR5IjoidmFsaWQiLCJkZWZhdWx0U3RhdGVWYWxpZGl0eSI6InZhbGlkIn0" +} \ No newline at end of file diff --git a/tests/assets/widgets/list_view_with_data.json b/tests/assets/widgets/list_view_with_data.json new file mode 100644 index 0000000..8816d20 --- /dev/null +++ b/tests/assets/widgets/list_view_with_data.json @@ -0,0 +1,77 @@ +{ + "type": "ListView", + "children": [ + { + "type": "ListViewItem", + "key": "blue", + "gap": 5, + "onClickAction": { + "type": "line.select", + "payload": { + "id": "blue" + } + }, + "children": [ + { + "type": "Box", + "background": "blue-500", + "radius": "full", + "size": 25 + }, + { + "type": "Text", + "value": "Blue line", + "size": "sm" + } + ] + }, + { + "type": "ListViewItem", + "key": "orange", + "gap": 5, + "onClickAction": { + "type": "line.select", + "payload": { + "id": "orange" + } + }, + "children": [ + { + "type": "Box", + "background": "orange-500", + "radius": "full", + "size": 25 + }, + { + "type": "Text", + "value": "Orange line", + "size": "sm" + } + ] + }, + { + "type": "ListViewItem", + "key": "purple", + "gap": 5, + "onClickAction": { + "type": "line.select", + "payload": { + "id": "purple" + } + }, + "children": [ + { + "type": "Box", + "background": "purple-500", + "radius": "full", + "size": 25 + }, + { + "type": "Text", + "value": "Purple line", + "size": "sm" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/assets/widgets/list_view_with_data.widget b/tests/assets/widgets/list_view_with_data.widget new file mode 100644 index 0000000..cc91d02 --- /dev/null +++ b/tests/assets/widgets/list_view_with_data.widget @@ -0,0 +1,121 @@ +{ + "version": "1.0", + "name": "list_view_with_data", + "template": "{\"type\":\"ListView\",\"children\":[{%- set _c -%}{%-for item in items -%},{\"type\":\"ListViewItem\",\"key\":{{ (item.id) | tojson }},\"gap\":5,\"onClickAction\":{\"type\":\"line.select\",\"payload\":{\"id\":{{ (item.id) | tojson }}}},\"children\":[{\"type\":\"Box\",\"background\":{{ (item.color) | tojson }},\"radius\":\"full\",\"size\":25},{\"type\":\"Text\",\"value\":{{ (item.label) | tojson }},\"size\":\"sm\"}]}{%-endfor-%}{%- endset -%}{{- (_c[1:] if _c and _c[0] == ',' else _c) -}}]}", + "jsonSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "color": { + "type": "string", + "enum": [ + "blue-500", + "orange-500", + "purple-500" + ] + } + }, + "required": [ + "id", + "label", + "color" + ], + "additionalProperties": false + } + } + }, + "required": [ + "items" + ], + "additionalProperties": false + }, + "outputJsonPreview": { + "type": "ListView", + "children": [ + { + "type": "ListViewItem", + "key": "blue", + "gap": 5, + "onClickAction": { + "type": "line.select", + "payload": { + "id": "blue" + } + }, + "children": [ + { + "type": "Box", + "background": "blue-500", + "radius": "full", + "size": 25 + }, + { + "type": "Text", + "value": "Blue line", + "size": "sm" + } + ] + }, + { + "type": "ListViewItem", + "key": "orange", + "gap": 5, + "onClickAction": { + "type": "line.select", + "payload": { + "id": "orange" + } + }, + "children": [ + { + "type": "Box", + "background": "orange-500", + "radius": "full", + "size": 25 + }, + { + "type": "Text", + "value": "Orange line", + "size": "sm" + } + ] + }, + { + "type": "ListViewItem", + "key": "purple", + "gap": 5, + "onClickAction": { + "type": "line.select", + "payload": { + "id": "purple" + } + }, + "children": [ + { + "type": "Box", + "background": "purple-500", + "radius": "full", + "size": 25 + }, + { + "type": "Text", + "value": "Purple line", + "size": "sm" + } + ] + } + ] + }, + "encodedWidget": "eyJpZCI6ImIyZTVmZTllLWVhOWItNGUyMy1iNjMxLTFmNTRhMDI4Mjg4MCIsIm5hbWUiOiJMaW5lIHNlbGVjdG9yIiwidmlldyI6IjxMaXN0Vmlldz5cbiAge2l0ZW1zLm1hcCgoaXRlbSkgPT4gKFxuICAgIDxMaXN0Vmlld0l0ZW1cbiAgICAgIGtleT17aXRlbS5pZH1cbiAgICAgIGdhcD17NX1cbiAgICAgIG9uQ2xpY2tBY3Rpb249e3sgdHlwZTogXCJsaW5lLnNlbGVjdFwiLCBwYXlsb2FkOiB7IGlkOiBpdGVtLmlkIH0gfX1cbiAgICA-XG4gICAgICA8Qm94IGJhY2tncm91bmQ9e2l0ZW0uY29sb3J9IHJhZGl1cz1cImZ1bGxcIiBzaXplPXsyNX0gLz5cbiAgICAgIDxUZXh0IHZhbHVlPXtpdGVtLmxhYmVsfSBzaXplPVwic21cIiAvPlxuICAgIDwvTGlzdFZpZXdJdGVtPlxuICApKX1cbjwvTGlzdFZpZXc-IiwiZGVmYXVsdFN0YXRlIjp7Iml0ZW1zIjpbeyJpZCI6ImJsdWUiLCJsYWJlbCI6IkJsdWUgbGluZSIsImNvbG9yIjoiYmx1ZS01MDAifSx7ImlkIjoib3JhbmdlIiwibGFiZWwiOiJPcmFuZ2UgbGluZSIsImNvbG9yIjoib3JhbmdlLTUwMCJ9LHsiaWQiOiJwdXJwbGUiLCJsYWJlbCI6IlB1cnBsZSBsaW5lIiwiY29sb3IiOiJwdXJwbGUtNTAwIn1dfSwic3RhdGVzIjpbXSwic2NoZW1hIjoiaW1wb3J0IHsgeiB9IGZyb20gXCJ6b2RcIlxuXG5jb25zdCBMaW5lSXRlbSA9IHouc3RyaWN0T2JqZWN0KHtcbiAgaWQ6IHouc3RyaW5nKCksXG4gIGxhYmVsOiB6LnN0cmluZygpLFxuICBjb2xvcjogei5lbnVtKFtcImJsdWUtNTAwXCIsIFwib3JhbmdlLTUwMFwiLCBcInB1cnBsZS01MDBcIl0pLFxufSlcblxuY29uc3QgV2lkZ2V0U3RhdGUgPSB6LnN0cmljdE9iamVjdCh7XG4gIGl0ZW1zOiB6LmFycmF5KExpbmVJdGVtKSxcbn0pXG5cbmV4cG9ydCBkZWZhdWx0IFdpZGdldFN0YXRlIiwic2NoZW1hTW9kZSI6InpvZCIsImpzb25TY2hlbWEiOnsidHlwZSI6Im9iamVjdCIsInByb3BlcnRpZXMiOnsidGl0bGUiOnsidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsidGl0bGUiXSwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlfSwic2NoZW1hVmFsaWRpdHkiOiJ2YWxpZCIsInZpZXdWYWxpZGl0eSI6InZhbGlkIiwiZGVmYXVsdFN0YXRlVmFsaWRpdHkiOiJ2YWxpZCJ9" +} \ No newline at end of file diff --git a/tests/test_widgets.py b/tests/test_widgets.py index dff8503..6fdc884 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -1,12 +1,30 @@ import json from datetime import datetime -from typing import Literal +from typing import Any, Literal import pytest from chatkit.server import diff_widget from chatkit.types import WidgetItem -from chatkit.widgets import Card, Text, WidgetRoot +from chatkit.widgets import ( + Card, + DynamicWidgetComponent, + DynamicWidgetRoot, + Text, + WidgetRoot, + WidgetTemplate, + env, +) + + +def dyn_comp(data: dict[str, Any]) -> DynamicWidgetComponent: + """Helper to build dynamic components while keeping type checkers happy.""" + return DynamicWidgetComponent.model_validate(data) + + +def to_template(payload: dict[str, Any]): + """Helper to build a Jinja template from a widget payload.""" + return env.from_string(json.dumps(payload)) @pytest.mark.parametrize( @@ -28,6 +46,83 @@ Card(children=[Text(value="world!")]), ["widget.root.updated"], ), + # DynamicWidgetRoot tests + ( + DynamicWidgetRoot(type="Card", children=[]), + DynamicWidgetRoot(type="Card", children=[]), + [], + ), + ( + DynamicWidgetRoot( + type="Card", + children=[ + DynamicWidgetComponent.model_validate({ + "type": "Text", + "id": "text", + "value": "Hello", + "streaming": True, + }) + ], + ), + DynamicWidgetRoot( + type="Card", + children=[ + DynamicWidgetComponent.model_validate({ + "type": "Text", + "id": "text", + "value": "Hello, world!", + "streaming": True, + }) + ], + ), + ["widget.streaming_text.value_delta"], + ), + ( + DynamicWidgetRoot( + type="Card", + children=[ + DynamicWidgetComponent.model_validate({ + "type": "Text", + "id": "text", + "value": "Hello", + "streaming": True, + }) + ], + ), + DynamicWidgetRoot( + type="Card", + children=[ + DynamicWidgetComponent.model_validate({ + "type": "Text", + "id": "text", + "value": "Hello, world!", + "streaming": False, + }) + ], + ), + ["widget.root.updated"], + ), + ( + DynamicWidgetRoot( + type="Card", + children=[ + DynamicWidgetComponent.model_validate({ + "type": "Text", + "value": "Hello", + }) + ], + ), + DynamicWidgetRoot( + type="Card", + children=[ + DynamicWidgetComponent.model_validate({ + "type": "Text", + "value": "world!", + }) + ], + ), + ["widget.root.updated"], + ), ], ) def test_diff( @@ -102,3 +197,58 @@ def test_json_dump_excludes_none_fields_nested(): assert "streaming" not in text_dump assert "color" not in text_dump assert "key" not in text_dump + + +@pytest.mark.parametrize( + "widget_name, data", + [ + ("list_view_no_data", None), + ("card_no_data", None), + ( + "list_view_with_data", + { + "items": [ + { + "id": "blue", + "label": "Blue line", + "color": "blue-500", + }, + { + "id": "orange", + "label": "Orange line", + "color": "orange-500", + }, + { + "id": "purple", + "label": "Purple line", + "color": "purple-500", + }, + ], + }, + ), + ( + "card_with_data", + { + "channel": "#proj-chatkit", + "time": "4:48 PM", + "user": { + "image": "/pam.png", + "name": "Pam Beesly", + }, + }, + ), + ], +) +def test_widget_template_from_file( + widget_name: str, + data: dict[str, Any] | None, +): + template = WidgetTemplate.from_file(f"assets/widgets/{widget_name}.widget") + + with open(f"tests/assets/widgets/{widget_name}.json", "r") as file: + expected_widget_dict = json.load(file) + + widget = template.build(data) + + assert isinstance(widget, DynamicWidgetRoot) + assert widget.model_dump(exclude_none=True) == expected_widget_dict diff --git a/uv.lock b/uv.lock index 45f7133..acbe51b 100644 --- a/uv.lock +++ b/uv.lock @@ -822,6 +822,7 @@ name = "openai-chatkit" version = "1.3.1" source = { virtual = "." } dependencies = [ + { name = "jinja2" }, { name = "openai" }, { name = "openai-agents" }, { name = "pydantic" }, @@ -851,6 +852,7 @@ lint = [ [package.metadata] requires-dist = [ + { name = "jinja2" }, { name = "openai" }, { name = "openai-agents", specifier = ">=0.3.2" }, { name = "pydantic" }, From 69ea9ef835b3bcb42f975d9142770a2c1201ec67 Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Fri, 21 Nov 2025 12:09:28 -0800 Subject: [PATCH 2/4] Deprecation warning --- chatkit/actions.py | 8 ++++++ chatkit/widgets.py | 70 +++++++++++++++++++++++++++++++--------------- 2 files changed, 56 insertions(+), 22 deletions(-) diff --git a/chatkit/actions.py b/chatkit/actions.py index e00b3ad..47e40b9 100644 --- a/chatkit/actions.py +++ b/chatkit/actions.py @@ -3,6 +3,7 @@ from typing import Any, Generic, Literal, TypeVar, get_args, get_origin from pydantic import BaseModel, Field +from typing_extensions import deprecated Handler = Literal["client", "server"] LoadingBehavior = Literal["auto", "none", "self", "container"] @@ -11,6 +12,12 @@ DEFAULT_LOADING_BEHAVIOR: LoadingBehavior = "auto" +direct_usage_of_action_classes_deprecated = deprecated( + "Direct usage of Action classes are deprecated; use WidgetTemplate to build widgets from .widget files/definitions instead." +) + + +@direct_usage_of_action_classes_deprecated class ActionConfig(BaseModel): type: str payload: Any = None @@ -22,6 +29,7 @@ class ActionConfig(BaseModel): TPayload = TypeVar("TPayload") +@direct_usage_of_action_classes_deprecated class Action(BaseModel, Generic[TType, TPayload]): type: TType = Field(default=TType, frozen=True) # pyright: ignore payload: TPayload = None # pyright: ignore - default to None to allow no-payload actions diff --git a/chatkit/widgets.py b/chatkit/widgets.py index 09083a5..a2d7b73 100644 --- a/chatkit/widgets.py +++ b/chatkit/widgets.py @@ -18,14 +18,19 @@ TypeAdapter, model_serializer, ) -from typing_extensions import NotRequired, TypedDict +from typing_extensions import NotRequired, TypedDict, deprecated from .actions import ActionConfig from .icons import IconName env = Environment(undefined=StrictUndefined) +direct_usage_of_named_widget_types_deprecated = deprecated( + "Direct usage of named widget classes are deprecated; use WidgetTemplate to build widgets from .widget files/definitions instead." +) + +@direct_usage_of_named_widget_types_deprecated class ThemeColor(TypedDict): """Color values for light and dark themes.""" @@ -35,6 +40,7 @@ class ThemeColor(TypedDict): """Color to use when the theme is light.""" +@direct_usage_of_named_widget_types_deprecated class Spacing(TypedDict): """Shorthand spacing values applied to a widget.""" @@ -52,6 +58,7 @@ class Spacing(TypedDict): """Vertical spacing; accepts a spacing unit or CSS string.""" +@direct_usage_of_named_widget_types_deprecated class Border(TypedDict): """Border style definition for an edge.""" @@ -72,6 +79,7 @@ class Border(TypedDict): """Border line style.""" +@direct_usage_of_named_widget_types_deprecated class Borders(TypedDict): """Composite border configuration applied across edges.""" @@ -89,6 +97,7 @@ class Borders(TypedDict): """Vertical borders or thickness in px.""" +@direct_usage_of_named_widget_types_deprecated class MinMax(TypedDict): """Integer minimum/maximum bounds.""" @@ -98,6 +107,7 @@ class MinMax(TypedDict): """Maximum value (inclusive).""" +@direct_usage_of_named_widget_types_deprecated class EditableProps(TypedDict): """Editable field options for text widgets.""" @@ -187,6 +197,7 @@ def serialize(self, next_): return dumped +@direct_usage_of_named_widget_types_deprecated class WidgetStatusWithFavicon(TypedDict): """Widget status representation using a favicon.""" @@ -198,6 +209,7 @@ class WidgetStatusWithFavicon(TypedDict): """Show a frame around the favicon for contrast.""" +@direct_usage_of_named_widget_types_deprecated class WidgetStatusWithIcon(TypedDict): """Widget status representation using an icon.""" @@ -211,6 +223,7 @@ class WidgetStatusWithIcon(TypedDict): """Union for representing widget status messaging.""" +@direct_usage_of_named_widget_types_deprecated class ListViewItem(WidgetComponentBase): """Single row inside a ``ListView`` component.""" @@ -225,6 +238,7 @@ class ListViewItem(WidgetComponentBase): """Y-axis alignment for content within the list item.""" +@direct_usage_of_named_widget_types_deprecated class ListView(WidgetComponentBase): """Container component for rendering collections of list items.""" @@ -239,6 +253,7 @@ class ListView(WidgetComponentBase): """Force light or dark theme for this subtree.""" +@direct_usage_of_named_widget_types_deprecated class CardAction(TypedDict): """Configuration for confirm/cancel actions within a card.""" @@ -248,6 +263,7 @@ class CardAction(TypedDict): """Declarative action dispatched to the host application.""" +@direct_usage_of_named_widget_types_deprecated class Card(WidgetComponentBase): """Versatile container used for structuring widget content.""" @@ -279,6 +295,7 @@ class Card(WidgetComponentBase): """Force light or dark theme for this subtree.""" +@direct_usage_of_named_widget_types_deprecated class Markdown(WidgetComponentBase): """Widget rendering Markdown content, optionally streamed.""" @@ -289,6 +306,7 @@ class Markdown(WidgetComponentBase): """Applies streaming-friendly transitions for incremental updates.""" +@direct_usage_of_named_widget_types_deprecated class Text(WidgetComponentBase): """Widget rendering plain text with typography controls.""" @@ -327,6 +345,7 @@ class Text(WidgetComponentBase): """Enable inline editing for this text node.""" +@direct_usage_of_named_widget_types_deprecated class Title(WidgetComponentBase): """Widget rendering prominent headline text.""" @@ -353,6 +372,7 @@ class Title(WidgetComponentBase): """Limit text to a maximum number of lines (line clamp).""" +@direct_usage_of_named_widget_types_deprecated class Caption(WidgetComponentBase): """Widget rendering supporting caption text.""" @@ -379,6 +399,7 @@ class Caption(WidgetComponentBase): """Limit text to a maximum number of lines (line clamp).""" +@direct_usage_of_named_widget_types_deprecated class Badge(WidgetComponentBase): """Small badge indicating status or categorization.""" @@ -397,6 +418,7 @@ class Badge(WidgetComponentBase): """Determines if the badge should be fully rounded (pill).""" +@direct_usage_of_named_widget_types_deprecated class BoxBase(BaseModel): """Shared layout props for flexible container widgets.""" @@ -449,6 +471,7 @@ class BoxBase(BaseModel): """Aspect ratio of the box (e.g., 16/9); number or CSS string.""" +@direct_usage_of_named_widget_types_deprecated class Box(WidgetComponentBase, BoxBase): """Generic flex container with direction control.""" @@ -457,18 +480,21 @@ class Box(WidgetComponentBase, BoxBase): """Flex direction for content within this container.""" +@direct_usage_of_named_widget_types_deprecated class Row(WidgetComponentBase, BoxBase): """Horizontal flex container.""" type: Literal["Row"] = Field(default="Row", frozen=True) # pyright: ignore +@direct_usage_of_named_widget_types_deprecated class Col(WidgetComponentBase, BoxBase): """Vertical flex container.""" type: Literal["Col"] = Field(default="Col", frozen=True) # pyright: ignore +@direct_usage_of_named_widget_types_deprecated class Form(WidgetComponentBase, BoxBase): """Form wrapper capable of submitting ``onSubmitAction``.""" @@ -479,6 +505,7 @@ class Form(WidgetComponentBase, BoxBase): """Flex direction for laying out form children.""" +@direct_usage_of_named_widget_types_deprecated class Divider(WidgetComponentBase): """Visual divider separating content sections.""" @@ -498,6 +525,7 @@ class Divider(WidgetComponentBase): """Flush the divider to the container edge, removing surrounding padding.""" +@direct_usage_of_named_widget_types_deprecated class Icon(WidgetComponentBase): """Icon component referencing a built-in icon name.""" @@ -516,6 +544,7 @@ class Icon(WidgetComponentBase): """Size of the icon; accepts an icon size token.""" +@direct_usage_of_named_widget_types_deprecated class Image(WidgetComponentBase): """Image component with sizing and fitting controls.""" @@ -580,6 +609,7 @@ class Image(WidgetComponentBase): """Flex growth/shrink factor.""" +@direct_usage_of_named_widget_types_deprecated class Button(WidgetComponentBase): """Button component optionally wired to an action.""" @@ -626,6 +656,7 @@ class Button(WidgetComponentBase): """Disable interactions and apply disabled styles.""" +@direct_usage_of_named_widget_types_deprecated class Spacer(WidgetComponentBase): """Flexible spacer used to push content apart.""" @@ -634,6 +665,7 @@ class Spacer(WidgetComponentBase): """Minimum size the spacer should occupy along the flex direction.""" +@direct_usage_of_named_widget_types_deprecated class SelectOption(TypedDict): """Selectable option used by the ``Select`` widget.""" @@ -647,6 +679,7 @@ class SelectOption(TypedDict): """Displayed as secondary text below the option `label`.""" +@direct_usage_of_named_widget_types_deprecated class Select(WidgetComponentBase): """Select dropdown component.""" @@ -675,6 +708,7 @@ class Select(WidgetComponentBase): """Disable interactions and apply disabled styles.""" +@direct_usage_of_named_widget_types_deprecated class DatePicker(WidgetComponentBase): """Date picker input component.""" @@ -709,6 +743,7 @@ class DatePicker(WidgetComponentBase): """Disable interactions and apply disabled styles.""" +@direct_usage_of_named_widget_types_deprecated class Checkbox(WidgetComponentBase): """Checkbox input component.""" @@ -727,6 +762,7 @@ class Checkbox(WidgetComponentBase): """Mark the checkbox as required for form submission.""" +@direct_usage_of_named_widget_types_deprecated class Input(WidgetComponentBase): """Single-line text input component.""" @@ -763,6 +799,7 @@ class Input(WidgetComponentBase): """Determines if the input should be fully rounded (pill).""" +@direct_usage_of_named_widget_types_deprecated class Label(WidgetComponentBase): """Form label associated with a field.""" @@ -787,6 +824,7 @@ class Label(WidgetComponentBase): """ +@direct_usage_of_named_widget_types_deprecated class RadioOption(TypedDict): """Option inside a ``RadioGroup`` widget.""" @@ -798,6 +836,7 @@ class RadioOption(TypedDict): """Disables a specific radio option.""" +@direct_usage_of_named_widget_types_deprecated class RadioGroup(WidgetComponentBase): """Grouped radio input control.""" @@ -820,6 +859,7 @@ class RadioGroup(WidgetComponentBase): """Mark the group as required for form submission.""" +@direct_usage_of_named_widget_types_deprecated class Textarea(WidgetComponentBase): """Multiline text input component.""" @@ -856,6 +896,7 @@ class Textarea(WidgetComponentBase): """Allow password managers / autofill extensions to appear.""" +@direct_usage_of_named_widget_types_deprecated class Transition(WidgetComponentBase): """Wrapper enabling transitions for a child component.""" @@ -864,6 +905,7 @@ class Transition(WidgetComponentBase): """The child component to animate layout changes for.""" +@direct_usage_of_named_widget_types_deprecated class Chart(WidgetComponentBase): """Data visualization component for simple bar/line/area charts.""" @@ -908,6 +950,7 @@ class Chart(WidgetComponentBase): """Aspect ratio of the chart area (e.g., 16/9); number or CSS string.""" +@direct_usage_of_named_widget_types_deprecated class XAxisConfig(TypedDict): """Configuration object for the X axis.""" @@ -939,6 +982,7 @@ class XAxisConfig(TypedDict): """Interpolation curve types for `area` and `line` series.""" +@direct_usage_of_named_widget_types_deprecated class BarSeries(BaseModel): """A bar series plotted from a numeric `dataKey`. Supports stacking.""" @@ -961,6 +1005,7 @@ class BarSeries(BaseModel): """ +@direct_usage_of_named_widget_types_deprecated class AreaSeries(BaseModel): """An area series plotted from a numeric `dataKey`. Supports stacking and curves.""" @@ -985,6 +1030,7 @@ class AreaSeries(BaseModel): """Interpolation curve type used to connect points.""" +@direct_usage_of_named_widget_types_deprecated class LineSeries(BaseModel): """A line series plotted from a numeric `dataKey`. Supports curves.""" @@ -1014,12 +1060,6 @@ class LineSeries(BaseModel): """Union of all supported chart series types.""" -WidgetRoot = Annotated[ - Card | ListView, - Field(discriminator="type"), -] - - class DynamicWidgetComponent(WidgetComponentBase): """ A widget component with a statically defined base shape but dynamically @@ -1071,24 +1111,10 @@ class DynamicWidgetRoot(DynamicWidgetComponent): type: Literal["Card", "ListView"] # pyright: ignore -class BasicRoot(WidgetComponentBase): +class BasicRoot(DynamicWidgetComponent): """Layout root capable of nesting components or other roots.""" type: Literal["Basic"] = Field(default="Basic", frozen=True) # pyright: ignore - children: list[WidgetComponent | WidgetRoot] - """Children to render inside this root. Can include widget components or nested roots.""" - theme: Literal["light", "dark"] | None = None - """Force light or dark theme for this subtree.""" - direction: Literal["row", "col"] | None = None - """Flex direction for laying out direct children.""" - gap: int | str | None = None - """Gap between direct children; spacing unit or CSS string.""" - padding: float | str | Spacing | None = None - """Inner padding; spacing unit, CSS string, or padding object.""" - align: Alignment | None = None - """Cross-axis alignment of children.""" - justify: Justification | None = None - """Main-axis distribution of children.""" WidgetComponent = StrictWidgetComponent | DynamicWidgetComponent From 1b15f7de44765f3dd01a304292b08ffe12c2574b Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Fri, 21 Nov 2025 14:34:20 -0800 Subject: [PATCH 3/4] Fix children type to be compatible with Transition; misc. cleanup --- chatkit/actions.py | 10 +++-- chatkit/widgets.py | 96 ++++++++++++++++++++++--------------------- tests/test_widgets.py | 11 ----- 3 files changed, 55 insertions(+), 62 deletions(-) diff --git a/chatkit/actions.py b/chatkit/actions.py index 47e40b9..b5c04d0 100644 --- a/chatkit/actions.py +++ b/chatkit/actions.py @@ -12,12 +12,14 @@ DEFAULT_LOADING_BEHAVIOR: LoadingBehavior = "auto" -direct_usage_of_action_classes_deprecated = deprecated( - "Direct usage of Action classes are deprecated; use WidgetTemplate to build widgets from .widget files/definitions instead." +_direct_usage_of_action_classes_deprecated = deprecated( + "Direct usage of named action classes is deprecated. " + "Use WidgetTemplate to build widgets from .widget files instead. " + "Visit https://widgets.chatkit.studio/ to author widget files." ) -@direct_usage_of_action_classes_deprecated +@_direct_usage_of_action_classes_deprecated class ActionConfig(BaseModel): type: str payload: Any = None @@ -29,7 +31,7 @@ class ActionConfig(BaseModel): TPayload = TypeVar("TPayload") -@direct_usage_of_action_classes_deprecated +@_direct_usage_of_action_classes_deprecated class Action(BaseModel, Generic[TType, TPayload]): type: TType = Field(default=TType, frozen=True) # pyright: ignore payload: TPayload = None # pyright: ignore - default to None to allow no-payload actions diff --git a/chatkit/widgets.py b/chatkit/widgets.py index a2d7b73..a0adecc 100644 --- a/chatkit/widgets.py +++ b/chatkit/widgets.py @@ -23,14 +23,16 @@ from .actions import ActionConfig from .icons import IconName -env = Environment(undefined=StrictUndefined) +_jinja_env = Environment(undefined=StrictUndefined) -direct_usage_of_named_widget_types_deprecated = deprecated( - "Direct usage of named widget classes are deprecated; use WidgetTemplate to build widgets from .widget files/definitions instead." +_direct_usage_of_named_widget_types_deprecated = deprecated( + "Direct usage of named widget classes is deprecated. " + "Use WidgetTemplate to build widgets from .widget files instead. " + "Visit https://widgets.chatkit.studio/ to author widget files." ) -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class ThemeColor(TypedDict): """Color values for light and dark themes.""" @@ -40,7 +42,7 @@ class ThemeColor(TypedDict): """Color to use when the theme is light.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Spacing(TypedDict): """Shorthand spacing values applied to a widget.""" @@ -58,7 +60,7 @@ class Spacing(TypedDict): """Vertical spacing; accepts a spacing unit or CSS string.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Border(TypedDict): """Border style definition for an edge.""" @@ -79,7 +81,7 @@ class Border(TypedDict): """Border line style.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Borders(TypedDict): """Composite border configuration applied across edges.""" @@ -97,7 +99,7 @@ class Borders(TypedDict): """Vertical borders or thickness in px.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class MinMax(TypedDict): """Integer minimum/maximum bounds.""" @@ -107,7 +109,7 @@ class MinMax(TypedDict): """Maximum value (inclusive).""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class EditableProps(TypedDict): """Editable field options for text widgets.""" @@ -197,7 +199,7 @@ def serialize(self, next_): return dumped -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class WidgetStatusWithFavicon(TypedDict): """Widget status representation using a favicon.""" @@ -209,7 +211,7 @@ class WidgetStatusWithFavicon(TypedDict): """Show a frame around the favicon for contrast.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class WidgetStatusWithIcon(TypedDict): """Widget status representation using an icon.""" @@ -223,7 +225,7 @@ class WidgetStatusWithIcon(TypedDict): """Union for representing widget status messaging.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class ListViewItem(WidgetComponentBase): """Single row inside a ``ListView`` component.""" @@ -238,7 +240,7 @@ class ListViewItem(WidgetComponentBase): """Y-axis alignment for content within the list item.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class ListView(WidgetComponentBase): """Container component for rendering collections of list items.""" @@ -253,7 +255,7 @@ class ListView(WidgetComponentBase): """Force light or dark theme for this subtree.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class CardAction(TypedDict): """Configuration for confirm/cancel actions within a card.""" @@ -263,7 +265,7 @@ class CardAction(TypedDict): """Declarative action dispatched to the host application.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Card(WidgetComponentBase): """Versatile container used for structuring widget content.""" @@ -295,7 +297,7 @@ class Card(WidgetComponentBase): """Force light or dark theme for this subtree.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Markdown(WidgetComponentBase): """Widget rendering Markdown content, optionally streamed.""" @@ -306,7 +308,7 @@ class Markdown(WidgetComponentBase): """Applies streaming-friendly transitions for incremental updates.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Text(WidgetComponentBase): """Widget rendering plain text with typography controls.""" @@ -345,7 +347,7 @@ class Text(WidgetComponentBase): """Enable inline editing for this text node.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Title(WidgetComponentBase): """Widget rendering prominent headline text.""" @@ -372,7 +374,7 @@ class Title(WidgetComponentBase): """Limit text to a maximum number of lines (line clamp).""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Caption(WidgetComponentBase): """Widget rendering supporting caption text.""" @@ -399,7 +401,7 @@ class Caption(WidgetComponentBase): """Limit text to a maximum number of lines (line clamp).""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Badge(WidgetComponentBase): """Small badge indicating status or categorization.""" @@ -418,7 +420,7 @@ class Badge(WidgetComponentBase): """Determines if the badge should be fully rounded (pill).""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class BoxBase(BaseModel): """Shared layout props for flexible container widgets.""" @@ -471,7 +473,7 @@ class BoxBase(BaseModel): """Aspect ratio of the box (e.g., 16/9); number or CSS string.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Box(WidgetComponentBase, BoxBase): """Generic flex container with direction control.""" @@ -480,21 +482,21 @@ class Box(WidgetComponentBase, BoxBase): """Flex direction for content within this container.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Row(WidgetComponentBase, BoxBase): """Horizontal flex container.""" type: Literal["Row"] = Field(default="Row", frozen=True) # pyright: ignore -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Col(WidgetComponentBase, BoxBase): """Vertical flex container.""" type: Literal["Col"] = Field(default="Col", frozen=True) # pyright: ignore -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Form(WidgetComponentBase, BoxBase): """Form wrapper capable of submitting ``onSubmitAction``.""" @@ -505,7 +507,7 @@ class Form(WidgetComponentBase, BoxBase): """Flex direction for laying out form children.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Divider(WidgetComponentBase): """Visual divider separating content sections.""" @@ -525,7 +527,7 @@ class Divider(WidgetComponentBase): """Flush the divider to the container edge, removing surrounding padding.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Icon(WidgetComponentBase): """Icon component referencing a built-in icon name.""" @@ -544,7 +546,7 @@ class Icon(WidgetComponentBase): """Size of the icon; accepts an icon size token.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Image(WidgetComponentBase): """Image component with sizing and fitting controls.""" @@ -609,7 +611,7 @@ class Image(WidgetComponentBase): """Flex growth/shrink factor.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Button(WidgetComponentBase): """Button component optionally wired to an action.""" @@ -656,7 +658,7 @@ class Button(WidgetComponentBase): """Disable interactions and apply disabled styles.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Spacer(WidgetComponentBase): """Flexible spacer used to push content apart.""" @@ -665,7 +667,7 @@ class Spacer(WidgetComponentBase): """Minimum size the spacer should occupy along the flex direction.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class SelectOption(TypedDict): """Selectable option used by the ``Select`` widget.""" @@ -679,7 +681,7 @@ class SelectOption(TypedDict): """Displayed as secondary text below the option `label`.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Select(WidgetComponentBase): """Select dropdown component.""" @@ -708,7 +710,7 @@ class Select(WidgetComponentBase): """Disable interactions and apply disabled styles.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class DatePicker(WidgetComponentBase): """Date picker input component.""" @@ -743,7 +745,7 @@ class DatePicker(WidgetComponentBase): """Disable interactions and apply disabled styles.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Checkbox(WidgetComponentBase): """Checkbox input component.""" @@ -762,7 +764,7 @@ class Checkbox(WidgetComponentBase): """Mark the checkbox as required for form submission.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Input(WidgetComponentBase): """Single-line text input component.""" @@ -799,7 +801,7 @@ class Input(WidgetComponentBase): """Determines if the input should be fully rounded (pill).""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Label(WidgetComponentBase): """Form label associated with a field.""" @@ -824,7 +826,7 @@ class Label(WidgetComponentBase): """ -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class RadioOption(TypedDict): """Option inside a ``RadioGroup`` widget.""" @@ -836,7 +838,7 @@ class RadioOption(TypedDict): """Disables a specific radio option.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class RadioGroup(WidgetComponentBase): """Grouped radio input control.""" @@ -859,7 +861,7 @@ class RadioGroup(WidgetComponentBase): """Mark the group as required for form submission.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Textarea(WidgetComponentBase): """Multiline text input component.""" @@ -896,7 +898,7 @@ class Textarea(WidgetComponentBase): """Allow password managers / autofill extensions to appear.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Transition(WidgetComponentBase): """Wrapper enabling transitions for a child component.""" @@ -905,7 +907,7 @@ class Transition(WidgetComponentBase): """The child component to animate layout changes for.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class Chart(WidgetComponentBase): """Data visualization component for simple bar/line/area charts.""" @@ -950,7 +952,7 @@ class Chart(WidgetComponentBase): """Aspect ratio of the chart area (e.g., 16/9); number or CSS string.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class XAxisConfig(TypedDict): """Configuration object for the X axis.""" @@ -982,7 +984,7 @@ class XAxisConfig(TypedDict): """Interpolation curve types for `area` and `line` series.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class BarSeries(BaseModel): """A bar series plotted from a numeric `dataKey`. Supports stacking.""" @@ -1005,7 +1007,7 @@ class BarSeries(BaseModel): """ -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class AreaSeries(BaseModel): """An area series plotted from a numeric `dataKey`. Supports stacking and curves.""" @@ -1030,7 +1032,7 @@ class AreaSeries(BaseModel): """Interpolation curve type used to connect points.""" -@direct_usage_of_named_widget_types_deprecated +@_direct_usage_of_named_widget_types_deprecated class LineSeries(BaseModel): """A line series plotted from a numeric `dataKey`. Supports curves.""" @@ -1067,7 +1069,7 @@ class DynamicWidgetComponent(WidgetComponentBase): """ model_config = ConfigDict(extra="allow") - children: list["DynamicWidgetComponent"] | None = None + children: DynamicWidgetComponent | list[DynamicWidgetComponent] | None = None StrictWidgetComponent = Annotated[ @@ -1157,7 +1159,7 @@ def __init__(self, definition: dict[str, Any]): if isinstance(template, Template): self.template = template else: - self.template = env.from_string(template) + self.template = _jinja_env.from_string(template) self.data_schema = definition.get("jsonSchema", {}) @classmethod diff --git a/tests/test_widgets.py b/tests/test_widgets.py index 6fdc884..02df23d 100644 --- a/tests/test_widgets.py +++ b/tests/test_widgets.py @@ -13,20 +13,9 @@ Text, WidgetRoot, WidgetTemplate, - env, ) -def dyn_comp(data: dict[str, Any]) -> DynamicWidgetComponent: - """Helper to build dynamic components while keeping type checkers happy.""" - return DynamicWidgetComponent.model_validate(data) - - -def to_template(payload: dict[str, Any]): - """Helper to build a Jinja template from a widget payload.""" - return env.from_string(json.dumps(payload)) - - @pytest.mark.parametrize( "before, after, expected", [ From d1110ab3a327620fefc20e494131239dde94851f Mon Sep 17 00:00:00 2001 From: Jiwon Kim Date: Fri, 21 Nov 2025 14:49:27 -0800 Subject: [PATCH 4/4] eof newlines --- tests/assets/widgets/card_no_data.json | 2 +- tests/assets/widgets/card_no_data.widget | 2 +- tests/assets/widgets/card_with_data.json | 2 +- tests/assets/widgets/card_with_data.widget | 2 +- tests/assets/widgets/list_view_no_data.json | 2 +- tests/assets/widgets/list_view_no_data.widget | 2 +- tests/assets/widgets/list_view_with_data.json | 2 +- tests/assets/widgets/list_view_with_data.widget | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/assets/widgets/card_no_data.json b/tests/assets/widgets/card_no_data.json index 995c11f..cfb801f 100644 --- a/tests/assets/widgets/card_no_data.json +++ b/tests/assets/widgets/card_no_data.json @@ -68,4 +68,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/tests/assets/widgets/card_no_data.widget b/tests/assets/widgets/card_no_data.widget index a4974e7..3d6353a 100644 --- a/tests/assets/widgets/card_no_data.widget +++ b/tests/assets/widgets/card_no_data.widget @@ -80,4 +80,4 @@ ] }, "encodedWidget": "eyJpZCI6IndpZ180YWtnYWg0YSIsIm5hbWUiOiJFbmFibGUgTm90aWZpY2F0aW9uIiwidmlldyI6IjxDYXJkPlxuICA8Q29sIGFsaWduPVwiY2VudGVyXCIgZ2FwPXs0fSBwYWRkaW5nPXs0fT5cbiAgICA8Qm94IGJhY2tncm91bmQ9XCJncmVlbi00MDBcIiByYWRpdXM9XCJmdWxsXCIgcGFkZGluZz17M30-XG4gICAgICA8SWNvbiBuYW1lPVwiY2hlY2tcIiBzaXplPVwiM3hsXCIgY29sb3I9XCJ3aGl0ZVwiIC8-XG4gICAgPC9Cb3g-XG4gICAgPENvbCBhbGlnbj1cImNlbnRlclwiIGdhcD17MX0-XG4gICAgICA8VGl0bGUgdmFsdWU9XCJFbmFibGUgbm90aWZpY2F0aW9uXCIgLz5cbiAgICAgIDxUZXh0IHZhbHVlPVwiTm90aWZ5IG1lIHdoZW4gdGhpcyBpdGVtIHNoaXBzXCIgY29sb3I9XCJzZWNvbmRhcnlcIiAvPlxuICAgIDwvQ29sPlxuICA8L0NvbD5cblxuICA8Um93PlxuICAgIDxCdXR0b25cbiAgICAgIGxhYmVsPVwiWWVzXCJcbiAgICAgIGJsb2NrXG4gICAgICBvbkNsaWNrQWN0aW9uPXt7XG4gICAgICAgIHR5cGU6IFwibm90aWZpY2F0aW9uLnNldHRpbmdzXCIsXG4gICAgICAgIHBheWxvYWQ6IHsgZW5hYmxlOiB0cnVlIH0sXG4gICAgICB9fVxuICAgIC8-XG4gICAgPEJ1dHRvblxuICAgICAgbGFiZWw9XCJOb1wiXG4gICAgICBibG9ja1xuICAgICAgdmFyaWFudD1cIm91dGxpbmVcIlxuICAgICAgb25DbGlja0FjdGlvbj17e1xuICAgICAgICB0eXBlOiBcIm5vdGlmaWNhdGlvbi5zZXR0aW5nc1wiLFxuICAgICAgICBwYXlsb2FkOiB7IGVuYWJsZTogdHJ1ZSB9LFxuICAgICAgfX1cbiAgICAvPlxuICA8L1Jvdz5cbjwvQ2FyZD4iLCJkZWZhdWx0U3RhdGUiOnt9LCJzY2hlbWFNb2RlIjoiem9kIiwianNvblNjaGVtYSI6e30sInNjaGVtYSI6ImltcG9ydCB7IHogfSBmcm9tIFwiem9kXCJcblxuY29uc3QgV2lkZ2V0U3RhdGUgPSB6Lm9iamVjdCh7fSlcblxuZXhwb3J0IGRlZmF1bHQgV2lkZ2V0U3RhdGUiLCJzdGF0ZXMiOltdLCJzY2hlbWFWYWxpZGl0eSI6InZhbGlkIiwidmlld1ZhbGlkaXR5IjoidmFsaWQiLCJkZWZhdWx0U3RhdGVWYWxpZGl0eSI6InZhbGlkIn0" -} \ No newline at end of file +} diff --git a/tests/assets/widgets/card_with_data.json b/tests/assets/widgets/card_with_data.json index a1d06f7..5602b2b 100644 --- a/tests/assets/widgets/card_with_data.json +++ b/tests/assets/widgets/card_with_data.json @@ -53,4 +53,4 @@ "type": "Spacer" } ] -} \ No newline at end of file +} diff --git a/tests/assets/widgets/card_with_data.widget b/tests/assets/widgets/card_with_data.widget index 6a07a48..922fa85 100644 --- a/tests/assets/widgets/card_with_data.widget +++ b/tests/assets/widgets/card_with_data.widget @@ -93,4 +93,4 @@ ] }, "encodedWidget": "eyJpZCI6IndpZ183N3AzNzIxeiIsIm5hbWUiOiJDaGFubmVsIG1lc3NhZ2UiLCJ2aWV3IjoiPENhcmQgc2l6ZT1cIm1kXCI-XG4gIDxSb3c-XG4gICAgPFRleHQgdmFsdWU9e2NoYW5uZWx9IC8-XG4gICAgPFNwYWNlciAvPlxuICAgIDxUZXh0IHZhbHVlPXt0aW1lfSBjb2xvcj1cInRlcnRpYXJ5XCIgLz5cbiAgPC9Sb3c-XG4gIDxEaXZpZGVyIGZsdXNoIC8-XG4gIDxSb3cgYWxpZ249XCJzdGFydFwiIGdhcD17NH0-XG4gICAgPEltYWdlIHNyYz17dXNlci5pbWFnZX0gc2l6ZT17NDR9IC8-XG4gICAgPENvbD5cbiAgICAgIDxUZXh0IHZhbHVlPXt1c2VyLm5hbWV9IHdlaWdodD1cInNlbWlib2xkXCIgLz5cbiAgICAgIDxNYXJrZG93blxuICAgICAgICB2YWx1ZT17YEVuZCBvZiB3ZWVrIHVwZGF0ZSBmb3IgQ2hhdEtpdDpcblxuMS4gRGVzaWduZWQgKipuZXcgaGVhZGVyIHN5c3RlbSoqIHdpdGggbW9yZSBmbGV4aWJpbGl0eSBmb3IgY3VzdG9tIG1lbnUgYWN0aW9ucy5cbjIuIE1hZGUgcHJvZ3Jlc3Mgb24gKipEZXZEYXkgdHJhaW5pbmcgbWF0ZXJpYWwqKi5cbjMuIENvb3JkaW5hdGVkIHdpdGggcGFydG5lcnMgdG8gKipwcmlvcml0aXplIHJlbWFpbmluZyBmZWF0dXJlIHJlcXVpcmVtZW50cyoqLlxuXG4qKk5leHQgd2VlayoqIEkgcGxhbiB0byBmb2N1cyBvbiBidWlsZGluZyBvdXQgb3VyIEZpZ21hIGxpYnJhcnkgYW5kIHVwZGF0aW5nIHRvIG5ldyBpY29ucy5gfVxuICAgICAgLz5cbiAgICA8L0NvbD5cbiAgPC9Sb3c-XG4gIDxTcGFjZXIgLz5cbjwvQ2FyZD4iLCJkZWZhdWx0U3RhdGUiOnsiY2hhbm5lbCI6IiNwcm9qLWNoYXRraXQiLCJ0aW1lIjoiNDo0OCBQTSIsInVzZXIiOnsiaW1hZ2UiOiIvemoucG5nIiwibmFtZSI6IlphY2ggSm9obnN0b24ifX0sInNjaGVtYU1vZGUiOiJ6b2QiLCJqc29uU2NoZW1hIjp7fSwic2NoZW1hIjoiaW1wb3J0IHsgeiB9IGZyb20gXCJ6b2RcIlxuXG5jb25zdCBVc2VyID0gei5vYmplY3Qoe1xuICBpbWFnZTogei5zdHJpbmcoKSxcbiAgbmFtZTogei5zdHJpbmcoKSxcbn0pXG5cbmNvbnN0IFdpZGdldFN0YXRlID0gei5vYmplY3Qoe1xuICBjaGFubmVsOiB6LnN0cmluZygpLFxuICB0aW1lOiB6LnN0cmluZygpLFxuICB1c2VyOiBVc2VyLFxufSlcblxuZXhwb3J0IGRlZmF1bHQgV2lkZ2V0U3RhdGUiLCJzdGF0ZXMiOltdLCJzY2hlbWFWYWxpZGl0eSI6InZhbGlkIiwidmlld1ZhbGlkaXR5IjoidmFsaWQiLCJkZWZhdWx0U3RhdGVWYWxpZGl0eSI6InZhbGlkIn0" -} \ No newline at end of file +} diff --git a/tests/assets/widgets/list_view_no_data.json b/tests/assets/widgets/list_view_no_data.json index fea7818..7900375 100644 --- a/tests/assets/widgets/list_view_no_data.json +++ b/tests/assets/widgets/list_view_no_data.json @@ -50,4 +50,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/tests/assets/widgets/list_view_no_data.widget b/tests/assets/widgets/list_view_no_data.widget index a6e272c..7dfdfab 100644 --- a/tests/assets/widgets/list_view_no_data.widget +++ b/tests/assets/widgets/list_view_no_data.widget @@ -62,4 +62,4 @@ ] }, "encodedWidget": "eyJpZCI6IndpZ18xN2U5dWtoaiIsIm5hbWUiOiJVbnRpdGxlZCB3aWRnZXQiLCJ2aWV3IjoiPExpc3RWaWV3PlxuICA8TGlzdFZpZXdJdGVtXG4gICAga2V5PVwiYmx1ZVwiXG4gICAgZ2FwPXs1fVxuICAgIG9uQ2xpY2tBY3Rpb249e3sgdHlwZTogXCJsaW5lLnNlbGVjdFwiLCBwYXlsb2FkOiB7IGlkOiBcImJsdWVcIiB9IH19XG4gID5cbiAgICA8Qm94IGJhY2tncm91bmQ9XCJibHVlLTUwMFwiIHJhZGl1cz1cImZ1bGxcIiBzaXplPXsyNX0gLz5cbiAgICA8VGV4dCB2YWx1ZT1cIkJsdWUgbGluZVwiIHNpemU9XCJzbVwiIC8-XG4gIDwvTGlzdFZpZXdJdGVtPlxuICA8TGlzdFZpZXdJdGVtXG4gICAga2V5PVwib3JhbmdlXCJcbiAgICBnYXA9ezV9XG4gICAgb25DbGlja0FjdGlvbj17eyB0eXBlOiBcImxpbmUuc2VsZWN0XCIsIHBheWxvYWQ6IHsgaWQ6IFwib3JhbmdlXCIgfSB9fVxuICA-XG4gICAgPEJveCBiYWNrZ3JvdW5kPVwib3JhbmdlLTUwMFwiIHJhZGl1cz1cImZ1bGxcIiBzaXplPXsyNX0gLz5cbiAgICA8VGV4dCB2YWx1ZT1cIk9yYW5nZSBsaW5lXCIgc2l6ZT1cInNtXCIgLz5cbiAgPC9MaXN0Vmlld0l0ZW0-XG48L0xpc3RWaWV3PiIsImRlZmF1bHRTdGF0ZSI6e30sInNjaGVtYU1vZGUiOiJ6b2QiLCJqc29uU2NoZW1hIjp7InR5cGUiOiJvYmplY3QiLCJwcm9wZXJ0aWVzIjp7InRpdGxlIjp7InR5cGUiOiJzdHJpbmcifX0sInJlcXVpcmVkIjpbInRpdGxlIl0sImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZX0sInNjaGVtYSI6ImltcG9ydCB7IHogfSBmcm9tIFwiem9kXCJcblxuY29uc3QgV2lkZ2V0U3RhdGUgPSB6LnN0cmljdE9iamVjdCh7fSlcblxuZXhwb3J0IGRlZmF1bHQgV2lkZ2V0U3RhdGUiLCJzdGF0ZXMiOltdLCJzY2hlbWFWYWxpZGl0eSI6InZhbGlkIiwidmlld1ZhbGlkaXR5IjoidmFsaWQiLCJkZWZhdWx0U3RhdGVWYWxpZGl0eSI6InZhbGlkIn0" -} \ No newline at end of file +} diff --git a/tests/assets/widgets/list_view_with_data.json b/tests/assets/widgets/list_view_with_data.json index 8816d20..bb5ab12 100644 --- a/tests/assets/widgets/list_view_with_data.json +++ b/tests/assets/widgets/list_view_with_data.json @@ -74,4 +74,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/tests/assets/widgets/list_view_with_data.widget b/tests/assets/widgets/list_view_with_data.widget index cc91d02..f5b541a 100644 --- a/tests/assets/widgets/list_view_with_data.widget +++ b/tests/assets/widgets/list_view_with_data.widget @@ -118,4 +118,4 @@ ] }, "encodedWidget": "eyJpZCI6ImIyZTVmZTllLWVhOWItNGUyMy1iNjMxLTFmNTRhMDI4Mjg4MCIsIm5hbWUiOiJMaW5lIHNlbGVjdG9yIiwidmlldyI6IjxMaXN0Vmlldz5cbiAge2l0ZW1zLm1hcCgoaXRlbSkgPT4gKFxuICAgIDxMaXN0Vmlld0l0ZW1cbiAgICAgIGtleT17aXRlbS5pZH1cbiAgICAgIGdhcD17NX1cbiAgICAgIG9uQ2xpY2tBY3Rpb249e3sgdHlwZTogXCJsaW5lLnNlbGVjdFwiLCBwYXlsb2FkOiB7IGlkOiBpdGVtLmlkIH0gfX1cbiAgICA-XG4gICAgICA8Qm94IGJhY2tncm91bmQ9e2l0ZW0uY29sb3J9IHJhZGl1cz1cImZ1bGxcIiBzaXplPXsyNX0gLz5cbiAgICAgIDxUZXh0IHZhbHVlPXtpdGVtLmxhYmVsfSBzaXplPVwic21cIiAvPlxuICAgIDwvTGlzdFZpZXdJdGVtPlxuICApKX1cbjwvTGlzdFZpZXc-IiwiZGVmYXVsdFN0YXRlIjp7Iml0ZW1zIjpbeyJpZCI6ImJsdWUiLCJsYWJlbCI6IkJsdWUgbGluZSIsImNvbG9yIjoiYmx1ZS01MDAifSx7ImlkIjoib3JhbmdlIiwibGFiZWwiOiJPcmFuZ2UgbGluZSIsImNvbG9yIjoib3JhbmdlLTUwMCJ9LHsiaWQiOiJwdXJwbGUiLCJsYWJlbCI6IlB1cnBsZSBsaW5lIiwiY29sb3IiOiJwdXJwbGUtNTAwIn1dfSwic3RhdGVzIjpbXSwic2NoZW1hIjoiaW1wb3J0IHsgeiB9IGZyb20gXCJ6b2RcIlxuXG5jb25zdCBMaW5lSXRlbSA9IHouc3RyaWN0T2JqZWN0KHtcbiAgaWQ6IHouc3RyaW5nKCksXG4gIGxhYmVsOiB6LnN0cmluZygpLFxuICBjb2xvcjogei5lbnVtKFtcImJsdWUtNTAwXCIsIFwib3JhbmdlLTUwMFwiLCBcInB1cnBsZS01MDBcIl0pLFxufSlcblxuY29uc3QgV2lkZ2V0U3RhdGUgPSB6LnN0cmljdE9iamVjdCh7XG4gIGl0ZW1zOiB6LmFycmF5KExpbmVJdGVtKSxcbn0pXG5cbmV4cG9ydCBkZWZhdWx0IFdpZGdldFN0YXRlIiwic2NoZW1hTW9kZSI6InpvZCIsImpzb25TY2hlbWEiOnsidHlwZSI6Im9iamVjdCIsInByb3BlcnRpZXMiOnsidGl0bGUiOnsidHlwZSI6InN0cmluZyJ9fSwicmVxdWlyZWQiOlsidGl0bGUiXSwiYWRkaXRpb25hbFByb3BlcnRpZXMiOmZhbHNlfSwic2NoZW1hVmFsaWRpdHkiOiJ2YWxpZCIsInZpZXdWYWxpZGl0eSI6InZhbGlkIiwiZGVmYXVsdFN0YXRlVmFsaWRpdHkiOiJ2YWxpZCJ9" -} \ No newline at end of file +}