diff --git a/.changeset/brave-otters-listen.md b/.changeset/brave-otters-listen.md new file mode 100644 index 00000000..4b929474 --- /dev/null +++ b/.changeset/brave-otters-listen.md @@ -0,0 +1,6 @@ +--- +'@modelscope-studio/antd': patch +'modelscope_studio': patch +--- + +feat: add `antd.BorderBeam` and `antd.Listy` components diff --git a/.changeset/bright-crabs-clap.md b/.changeset/bright-crabs-clap.md new file mode 100644 index 00000000..394b5367 --- /dev/null +++ b/.changeset/bright-crabs-clap.md @@ -0,0 +1,10 @@ +--- +'@modelscope-studio/antdx': patch +'modelscope_studio': patch +--- + +feat: sync antdx APIs from 2.7.0 to 2.9.0 + +- `Folder`: new `context_menu` (items or JS function string) and `right_click` event +- `Folder.TreeNode`: new `context_menu` (items or `False` to disable per-node) +- `Think` and `ThoughtChain.ThoughtChainItem`: new `destroy_on_hidden` diff --git a/.changeset/clean-brushes-drum.md b/.changeset/clean-brushes-drum.md new file mode 100644 index 00000000..091a8505 --- /dev/null +++ b/.changeset/clean-brushes-drum.md @@ -0,0 +1,6 @@ +--- +'@modelscope-studio/frontend': patch +'modelscope_studio': patch +--- + +fix: refit the frontend vite plugin for Vite 8 + rolldown diff --git a/.changeset/fresh-shoes-jog.md b/.changeset/fresh-shoes-jog.md new file mode 100644 index 00000000..97e9a06c --- /dev/null +++ b/.changeset/fresh-shoes-jog.md @@ -0,0 +1,15 @@ +--- +'@modelscope-studio/antd': patch +'modelscope_studio': patch +--- + +feat: sync antd APIs from 6.4.2 to 6.6.0 + +- New `clear` event on `DatePicker`, `DatePicker.RangePicker`, `TimePicker`, `TimePicker.RangePicker` +- New props: `Modal.scroll_lock` / `Modal.Static.scroll_lock`, `Steps.max_count`, `FloatButton.BackTop.show_progress`, `Mentions.popup_render` +- New slots: `Tabs` `more.popupRender`, `Pagination` / `Table` `components.sizeChanger` +- `Slider.disabled` accepts a list, `Watermark.content` accepts per-line font styles, `Select.token_separators` accepts a JS function string +- `Dropdown` / `Dropdown.Button` `placement` completed to the full set +- `ConfigProvider` new locale `sq_AL` +- Add `Input.Password.visibility_toggle` (with `visiable_toggle` kept as a compatibility alias) +- Fix a missing comma in `Table` `SLOTS` that dropped one slot diff --git a/.changeset/lucky-taxis-serve.md b/.changeset/lucky-taxis-serve.md new file mode 100644 index 00000000..ddfd7ac4 --- /dev/null +++ b/.changeset/lucky-taxis-serve.md @@ -0,0 +1,6 @@ +--- +'@modelscope-studio/frontend': patch +'modelscope_studio': patch +--- + +feat: vendor `@gradio/preview` to `patches/gradio-preview` to support both Gradio bundle generations, and fix the `svelte-i18n` 404 and broken `gradio cc dev` boot along the way diff --git a/.changeset/silly-doors-bathe.md b/.changeset/silly-doors-bathe.md new file mode 100644 index 00000000..e913a614 --- /dev/null +++ b/.changeset/silly-doors-bathe.md @@ -0,0 +1,11 @@ +--- +'@modelscope-studio/antd': patch +'@modelscope-studio/antdx': patch +'@modelscope-studio/base': patch +'@modelscope-studio/pro': patch +'@modelscope-studio/frontend': patch +'@modelscope-studio/lint-config': patch +'modelscope_studio': patch +--- + +chore: upgrade antd to 6.6.0, antdx to 2.9.0 and refresh the build stack (Vite 8 + rolldown, React 19.2.8, svelte 5.56.8, Babel 8, monaco-editor 0.56, TypeScript 6, Gradio 6.8.0) diff --git a/.changeset/tidy-donkeys-thank.md b/.changeset/tidy-donkeys-thank.md new file mode 100644 index 00000000..0ebc2191 --- /dev/null +++ b/.changeset/tidy-donkeys-thank.md @@ -0,0 +1,5 @@ +--- +'@modelscope-studio/changelog': patch +--- + +fix(changelog): migrate to the `@changesets/get-github-info` v1 API (`getCommitInfo` / `getPullRequestInfo`) diff --git a/.flake8 b/.flake8 index 9cab1c7d..adebd013 100644 --- a/.flake8 +++ b/.flake8 @@ -1,6 +1,7 @@ [flake8] exclude = node_modules/ + patches/ max-line-length = 9999 avoid-escape = no ignore = diff --git a/.gitignore b/.gitignore index b8470bf2..a9d1cdb9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ __pycache__/ __tmp/* *.pyi templates - +/gradio # common !.*ignore diff --git a/.prettierignore b/.prettierignore index 9c5a8ddd..76a9b2c5 100644 --- a/.prettierignore +++ b/.prettierignore @@ -150,3 +150,4 @@ pnpm-lock.yaml frontend/compiled/build-assets templates +patches diff --git a/.wiki/en/API Reference/Python API/Antd Components API/Data Display Components API.md b/.wiki/en/API Reference/Python API/Antd Components API/Data Display Components API.md index 7a2033ee..f835d569 100644 --- a/.wiki/en/API Reference/Python API/Antd Components API/Data Display Components API.md +++ b/.wiki/en/API Reference/Python API/Antd Components API/Data Display Components API.md @@ -428,7 +428,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **Section sources** -- [backend/modelscope_studio/components/antd/segmented/__init__.py] +- [backend/modelscope_studio/components/antd/segmented/**init**.py] ### Statistic @@ -441,7 +441,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **Section sources** -- [backend/modelscope_studio/components/antd/statistic/__init__.py] +- [backend/modelscope_studio/components/antd/statistic/**init**.py] ### Table @@ -454,7 +454,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **Section sources** -- [backend/modelscope_studio/components/antd/table/__init__.py] +- [backend/modelscope_studio/components/antd/table/**init**.py] ### Tabs @@ -467,7 +467,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **Section sources** -- [backend/modelscope_studio/components/antd/tabs/__init__.py] +- [backend/modelscope_studio/components/antd/tabs/**init**.py] ### Tag @@ -513,7 +513,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **Section sources** -- [backend/modelscope_studio/components/antd/tooltip/__init__.py] +- [backend/modelscope_studio/components/antd/tooltip/**init**.py] ### Tour @@ -526,7 +526,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **Section sources** -- [backend/modelscope_studio/components/antd/tour/__init__.py] +- [backend/modelscope_studio/components/antd/tour/**init**.py] ### Tree @@ -539,7 +539,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **Section sources** -- [backend/modelscope_studio/components/antd/tree/__init__.py] +- [backend/modelscope_studio/components/antd/tree/**init**.py] ### Checkable Tag Group Components diff --git "a/.wiki/zh/API \345\217\202\350\200\203/Python API/Antd \347\273\204\344\273\266 API/\346\225\260\346\215\256\345\261\225\347\244\272\347\273\204\344\273\266 API.md" "b/.wiki/zh/API \345\217\202\350\200\203/Python API/Antd \347\273\204\344\273\266 API/\346\225\260\346\215\256\345\261\225\347\244\272\347\273\204\344\273\266 API.md" index bc2f0f46..e41d5ebd 100644 --- "a/.wiki/zh/API \345\217\202\350\200\203/Python API/Antd \347\273\204\344\273\266 API/\346\225\260\346\215\256\345\261\225\347\244\272\347\273\204\344\273\266 API.md" +++ "b/.wiki/zh/API \345\217\202\350\200\203/Python API/Antd \347\273\204\344\273\266 API/\346\225\260\346\215\256\345\261\225\347\244\272\347\273\204\344\273\266 API.md" @@ -428,7 +428,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **章节来源** -- [backend/modelscope_studio/components/antd/segmented/__init__.py] +- [backend/modelscope_studio/components/antd/segmented/**init**.py] ### Statistic 计数 @@ -441,7 +441,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **章节来源** -- [backend/modelscope_studio/components/antd/statistic/__init__.py] +- [backend/modelscope_studio/components/antd/statistic/**init**.py] ### Table 表格 @@ -454,7 +454,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **章节来源** -- [backend/modelscope_studio/components/antd/table/__init__.py] +- [backend/modelscope_studio/components/antd/table/**init**.py] ### Tabs 标签页 @@ -467,7 +467,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **章节来源** -- [backend/modelscope_studio/components/antd/tabs/__init__.py] +- [backend/modelscope_studio/components/antd/tabs/**init**.py] ### Tag 标签 @@ -513,7 +513,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **章节来源** -- [backend/modelscope_studio/components/antd/tooltip/__init__.py] +- [backend/modelscope_studio/components/antd/tooltip/**init**.py] ### Tour 导览 @@ -526,7 +526,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **章节来源** -- [backend/modelscope_studio/components/antd/tour/__init__.py] +- [backend/modelscope_studio/components/antd/tour/**init**.py] ### Tree 树形控件 @@ -539,7 +539,7 @@ ModelScopeLayoutComponent <|-- AntdTimeline **章节来源** -- [backend/modelscope_studio/components/antd/tree/__init__.py] +- [backend/modelscope_studio/components/antd/tree/**init**.py] ### 可勾选标签组组件 diff --git a/backend/modelscope_studio/components/antd/__init__.py b/backend/modelscope_studio/components/antd/__init__.py index e4ade5ad..52c49d01 100644 --- a/backend/modelscope_studio/components/antd/__init__.py +++ b/backend/modelscope_studio/components/antd/__init__.py @@ -9,6 +9,7 @@ from .avatar.group import AntdAvatarGroup as AvatarGroup from .badge import AntdBadge as Badge from .badge.ribbon import AntdBadgeRibbon as BadgeRibbon +from .border_beam import AntdBorderBeam as BorderBeam from .breadcrumb import AntdBreadcrumb as Breadcrumb from .breadcrumb.item import AntdBreadcrumbItem as BreadcrumbItem from .button import AntdButton as Button @@ -70,6 +71,7 @@ from .list import AntdList as List from .list.item import AntdListItem as ListItem from .list.item.meta import AntdListItemMeta as ListItemMeta +from .listy import AntdListy as Listy from .masonry import AntdMasonry as Masonry from .masonry.item import AntdMasonryItem as MasonryItem from .mentions import AntdMentions as Mentions diff --git a/backend/modelscope_studio/components/antd/border_beam/__init__.py b/backend/modelscope_studio/components/antd/border_beam/__init__.py new file mode 100644 index 00000000..0ec8d88a --- /dev/null +++ b/backend/modelscope_studio/components/antd/border_beam/__init__.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +from typing import Any + +from ....utils.dev import ModelScopeLayoutComponent, resolve_frontend_dir + + +class AntdBorderBeam(ModelScopeLayoutComponent): + """ + Ant Design: https://ant.design/components/border-beam + + A decorative component that renders a flowing highlight animation along the + border of its child container. + + When to use: + - Draw attention to a card, button or panel without changing its layout. + - Indicate an ongoing or AI-generated process in an ambient way. + """ + + EVENTS = [] + + def __init__( + self, + additional_props: dict | None = None, + *, + color: str | list[dict] | None = None, + count: int | None = None, + duration: int | float | None = None, + line_width: int | float | str | None = None, + outset: int | float | str | None = None, + size: int | float | str | None = None, + as_item: str | None = None, + _internal: None = None, + # gradio properties + visible: bool = True, + elem_id: str | None = None, + elem_classes: list[str] | str | None = None, + elem_style: dict | None = None, + render: bool = True, + **kwargs): + super().__init__(visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + as_item=as_item, + elem_style=elem_style, + **kwargs) + self.additional_props = additional_props + self.color = color + self.count = count + self.duration = duration + self.line_width = line_width + self.outset = outset + self.size = size + + FRONTEND_DIR = resolve_frontend_dir("border-beam") + + @property + def skip_api(self): + return True + + def preprocess(self, payload: None) -> None: + return payload + + def postprocess(self, value: None) -> None: + return value + + def example_payload(self) -> Any: + return None + + def example_value(self) -> Any: + return None diff --git a/backend/modelscope_studio/components/antd/components.py b/backend/modelscope_studio/components/antd/components.py index 2a721e34..70f9eee9 100644 --- a/backend/modelscope_studio/components/antd/components.py +++ b/backend/modelscope_studio/components/antd/components.py @@ -9,6 +9,7 @@ from .avatar.group import AntdAvatarGroup from .badge import AntdBadge from .badge.ribbon import AntdBadgeRibbon +from .border_beam import AntdBorderBeam from .breadcrumb import AntdBreadcrumb from .breadcrumb.item import AntdBreadcrumbItem from .button import AntdButton @@ -67,6 +68,7 @@ from .list import AntdList from .list.item import AntdListItem from .list.item.meta import AntdListItemMeta +from .listy import AntdListy from .masonry import AntdMasonry from .masonry.item import AntdMasonryItem from .mentions import AntdMentions diff --git a/backend/modelscope_studio/components/antd/config_provider/__init__.py b/backend/modelscope_studio/components/antd/config_provider/__init__.py index 4b6bdf82..7c0e7a03 100644 --- a/backend/modelscope_studio/components/antd/config_provider/__init__.py +++ b/backend/modelscope_studio/components/antd/config_provider/__init__.py @@ -14,9 +14,9 @@ 'ko_KR', 'ku_IQ', 'lt_LT', 'lv_LV', 'mk_MK', 'ml_IN', 'mn_MN', 'ms_MY', 'my_MM', 'nb_NO', 'ne_NP', 'nl_BE', 'nl_NL', 'pl_PL', 'pt_BR', 'pt_PT', 'ro_RO', 'ru_RU', - 'si_LK', 'sk_SK', 'sl_SI', 'sr_RS', 'sv_SE', 'ta_IN', - 'th_TH', 'tk_TK', 'tr_TR', 'uk_UA', 'ur_PK', 'uz_UZ', - 'vi_VN', 'zh_CN', 'zh_HK', 'zh_TW'] + 'si_LK', 'sk_SK', 'sl_SI', 'sq_AL', 'sr_RS', 'sv_SE', + 'ta_IN', 'th_TH', 'tk_TK', 'tr_TR', 'uk_UA', 'ur_PK', + 'uz_UZ', 'vi_VN', 'zh_CN', 'zh_HK', 'zh_TW'] class AntdConfigProvider(ModelScopeLayoutComponent): diff --git a/backend/modelscope_studio/components/antd/date_picker/__init__.py b/backend/modelscope_studio/components/antd/date_picker/__init__.py index 02f36891..26015a71 100644 --- a/backend/modelscope_studio/components/antd/date_picker/__init__.py +++ b/backend/modelscope_studio/components/antd/date_picker/__init__.py @@ -30,6 +30,9 @@ class AntdDatePicker(ModelScopeDataLayoutComponent): EventListener("open_change", callback=lambda block: block._internal.update( bind_openChange_event=True)), + EventListener("clear", + callback=lambda block: block._internal.update( + bind_clear_event=True)), ] # supported slots diff --git a/backend/modelscope_studio/components/antd/date_picker/range_picker/__init__.py b/backend/modelscope_studio/components/antd/date_picker/range_picker/__init__.py index bc8c8cff..72b418a8 100644 --- a/backend/modelscope_studio/components/antd/date_picker/range_picker/__init__.py +++ b/backend/modelscope_studio/components/antd/date_picker/range_picker/__init__.py @@ -26,6 +26,9 @@ class AntdDatePickerRangePicker(ModelScopeDataLayoutComponent): EventListener("blur", callback=lambda block: block._internal.update( bind_blur_event=True)), + EventListener("clear", + callback=lambda block: block._internal.update( + bind_clear_event=True)), ] # supported slots diff --git a/backend/modelscope_studio/components/antd/dropdown/__init__.py b/backend/modelscope_studio/components/antd/dropdown/__init__.py index 8e4fb6fb..2856809c 100644 --- a/backend/modelscope_studio/components/antd/dropdown/__init__.py +++ b/backend/modelscope_studio/components/antd/dropdown/__init__.py @@ -53,8 +53,11 @@ def __init__( menu: dict | None = None, overlay_class_name: str | None = None, overlay_style: dict | None = None, - placement: Literal['topLeft', 'top', 'topRight', 'bottomLeft', - 'bottom', 'bottomRight'] = "bottomLeft", + placement: Literal['topLeft', 'topCenter', 'topRight', + 'bottomLeft', 'bottomCenter', 'bottomRight', + 'top', 'bottom', 'left', 'leftTop', + 'leftBottom', 'right', 'rightTop', + 'rightBottom'] = "bottomLeft", trigger: list[Literal['click', 'hover', 'contextMenu']] = ['hover'], open: bool | None = None, diff --git a/backend/modelscope_studio/components/antd/dropdown/button/__init__.py b/backend/modelscope_studio/components/antd/dropdown/button/__init__.py index 50a26f02..0251c264 100644 --- a/backend/modelscope_studio/components/antd/dropdown/button/__init__.py +++ b/backend/modelscope_studio/components/antd/dropdown/button/__init__.py @@ -56,8 +56,11 @@ def __init__( menu: dict | None = None, overlay_class_name: str | None = None, overlay_style: dict | None = None, - placement: Literal['topLeft', 'top', 'topRight', 'bottomLeft', - 'bottom', 'bottomRight'] = "bottomLeft", + placement: Literal['topLeft', 'topCenter', 'topRight', + 'bottomLeft', 'bottomCenter', 'bottomRight', + 'top', 'bottom', 'left', 'leftTop', + 'leftBottom', 'right', 'rightTop', + 'rightBottom'] = "bottomLeft", trigger: list[Literal['click', 'hover', 'contextMenu']] = ['hover'], open: bool | None = None, diff --git a/backend/modelscope_studio/components/antd/float_button/back_top/__init__.py b/backend/modelscope_studio/components/antd/float_button/back_top/__init__.py index 89c73443..f00c7ef5 100644 --- a/backend/modelscope_studio/components/antd/float_button/back_top/__init__.py +++ b/backend/modelscope_studio/components/antd/float_button/back_top/__init__.py @@ -45,6 +45,7 @@ def __init__( badge: dict | None = None, duration: int | float = 450, get_target: str | None = None, + show_progress: bool | None = None, visibility_height: int | float = 400, root_class_name: str | None = None, class_names: dict | str | None = None, @@ -93,6 +94,7 @@ def __init__( self.duration = duration self.visibility_height = visibility_height self.get_target = get_target + self.show_progress = show_progress self.root_class_name = root_class_name FRONTEND_DIR = resolve_frontend_dir("float-button", "back-top") diff --git a/backend/modelscope_studio/components/antd/input/password/__init__.py b/backend/modelscope_studio/components/antd/input/password/__init__.py index 0f6a4665..74b0deb7 100644 --- a/backend/modelscope_studio/components/antd/input/password/__init__.py +++ b/backend/modelscope_studio/components/antd/input/password/__init__.py @@ -39,7 +39,7 @@ def __init__( additional_props: dict | None = None, *, icon_render: str | None = None, - visiable_toggle: bool | dict | None = True, + visibility_toggle: bool | dict | None = None, addon_after: str | None = None, addon_before: str | None = None, allow_clear: bool | dict | None = None, @@ -79,7 +79,7 @@ def __init__( **kwargs) self.additional_props = additional_props self.icon_render = icon_render - self.visiable_toggle = visiable_toggle + self.visibility_toggle = visibility_toggle self.addon_after = addon_after self.addon_before = addon_before self.allow_clear = allow_clear diff --git a/backend/modelscope_studio/components/antd/listy/__init__.py b/backend/modelscope_studio/components/antd/listy/__init__.py new file mode 100644 index 00000000..9935060a --- /dev/null +++ b/backend/modelscope_studio/components/antd/listy/__init__.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import Any + +from gradio.events import EventListener + +from ....utils.dev import ModelScopeLayoutComponent, resolve_frontend_dir + + +class AntdListy(ModelScopeLayoutComponent): + """ + Ant Design: https://ant.design/components/listy + + A high performance list component which renders large data sets with + virtual scrolling. + + When to use: + - When rendering a long list where the built-in List component becomes slow. + - When list items should be grouped with sticky group headers. + """ + + EVENTS = [ + EventListener("scroll", + callback=lambda block: block._internal.update( + bind_scroll_event=True)), + ] + + # supported slots + SLOTS = ['itemRender', 'group.title'] + + def __init__( + self, + items: list[dict] | None = None, + additional_props: dict | None = None, + *, + row_key: str | None = None, + item_render: str | None = None, + group: dict | None = None, + height: int | float | None = None, + sticky: bool | None = None, + virtual: bool | None = None, + root_class_name: str | None = None, + class_names: dict | str | None = None, + styles: dict | str | None = None, + as_item: str | None = None, + _internal: None = None, + # gradio properties + visible: bool = True, + elem_id: str | None = None, + elem_classes: list[str] | str | None = None, + elem_style: dict | None = None, + render: bool = True, + **kwargs): + super().__init__(visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + as_item=as_item, + elem_style=elem_style, + **kwargs) + self.additional_props = additional_props + self.items = items + self.row_key = row_key + self.item_render = item_render + self.group = group + self.height = height + self.sticky = sticky + self.virtual = virtual + self.root_class_name = root_class_name + self.class_names = class_names + self.styles = styles + + FRONTEND_DIR = resolve_frontend_dir("listy") + + @property + def skip_api(self): + return True + + def preprocess(self, payload: None) -> None: + return payload + + def postprocess(self, value: None) -> None: + return value + + def example_payload(self) -> Any: + return None + + def example_value(self) -> Any: + return None diff --git a/backend/modelscope_studio/components/antd/mentions/__init__.py b/backend/modelscope_studio/components/antd/mentions/__init__.py index d7b04d25..1515d2a6 100644 --- a/backend/modelscope_studio/components/antd/mentions/__init__.py +++ b/backend/modelscope_studio/components/antd/mentions/__init__.py @@ -43,7 +43,9 @@ class AntdMentions(ModelScopeDataLayoutComponent): ] # supported slots - SLOTS = ['allowClear.clearIcon', 'notFoundContent', "options"] + SLOTS = [ + 'allowClear.clearIcon', 'notFoundContent', 'popupRender', "options" + ] def __init__( self, @@ -60,6 +62,7 @@ def __init__( read_only: bool | None = None, get_popup_container: str | None = None, popup_class_name: str | None = None, + popup_render: str | None = None, not_found_content: str | None = None, options: list[dict] | None = None, loading: bool | None = None, @@ -101,6 +104,7 @@ def __init__( self.read_only = read_only self.get_popup_container = get_popup_container self.popup_class_name = popup_class_name + self.popup_render = popup_render self.not_found_content = not_found_content self.options = options self.placeholder = placeholder diff --git a/backend/modelscope_studio/components/antd/modal/__init__.py b/backend/modelscope_studio/components/antd/modal/__init__.py index af757942..1ccba1cc 100644 --- a/backend/modelscope_studio/components/antd/modal/__init__.py +++ b/backend/modelscope_studio/components/antd/modal/__init__.py @@ -55,6 +55,7 @@ def __init__( mask: bool | None = None, mask_closable: bool | None = None, modal_render: str | None = None, + scroll_lock: bool | None = None, ok_text: str | None = None, ok_type: str | None = None, ok_button_props: dict | None = None, @@ -103,6 +104,7 @@ def __init__( self.mask = mask self.mask_closable = mask_closable self.modal_render = modal_render + self.scroll_lock = scroll_lock self.ok_text = ok_text self.ok_type = ok_type self.ok_button_props = ok_button_props diff --git a/backend/modelscope_studio/components/antd/modal/static/__init__.py b/backend/modelscope_studio/components/antd/modal/static/__init__.py index 534ed4b2..67842a84 100644 --- a/backend/modelscope_studio/components/antd/modal/static/__init__.py +++ b/backend/modelscope_studio/components/antd/modal/static/__init__.py @@ -54,6 +54,7 @@ def __init__( mask: bool | None = None, mask_closable: bool | None = None, modal_render: str | None = None, + scroll_lock: bool | None = None, ok_text: str | None = None, ok_type: str | None = None, ok_button_props: dict | None = None, @@ -103,6 +104,7 @@ def __init__( self.mask = mask self.mask_closable = mask_closable self.modal_render = modal_render + self.scroll_lock = scroll_lock self.ok_text = ok_text self.ok_type = ok_type self.ok_button_props = ok_button_props diff --git a/backend/modelscope_studio/components/antd/pagination/__init__.py b/backend/modelscope_studio/components/antd/pagination/__init__.py index d39812ce..a55bb0b4 100644 --- a/backend/modelscope_studio/components/antd/pagination/__init__.py +++ b/backend/modelscope_studio/components/antd/pagination/__init__.py @@ -21,7 +21,9 @@ class AntdPagination(ModelScopeLayoutComponent): ] # supported slots - SLOTS = ['showQuickJumper.goButton', 'itemRender'] + SLOTS = [ + 'showQuickJumper.goButton', 'itemRender', 'components.sizeChanger' + ] def __init__( self, diff --git a/backend/modelscope_studio/components/antd/select/__init__.py b/backend/modelscope_studio/components/antd/select/__init__.py index 3f5c35ba..9c4be215 100644 --- a/backend/modelscope_studio/components/antd/select/__init__.py +++ b/backend/modelscope_studio/components/antd/select/__init__.py @@ -105,7 +105,7 @@ def __init__( prefix: str | None = None, tag_render: str | None = None, label_render: str | None = None, - token_separators: list[str] | None = None, + token_separators: list[str] | str | None = None, variant: Literal['outlined', 'borderless', 'filled', 'underlined'] | None = None, virtual: bool | None = True, diff --git a/backend/modelscope_studio/components/antd/slider/__init__.py b/backend/modelscope_studio/components/antd/slider/__init__.py index c042f339..f3bd0f2e 100644 --- a/backend/modelscope_studio/components/antd/slider/__init__.py +++ b/backend/modelscope_studio/components/antd/slider/__init__.py @@ -37,7 +37,7 @@ def __init__( class_names: dict | str | None = None, default_value: int | float | tuple[int | float, int | float] | list[int | float] | None = None, - disabled: bool | None = None, + disabled: bool | list[bool] | None = None, dots: bool | None = None, included: bool | None = True, keyboard: bool | None = True, diff --git a/backend/modelscope_studio/components/antd/steps/__init__.py b/backend/modelscope_studio/components/antd/steps/__init__.py index 5ae34c59..b250994f 100644 --- a/backend/modelscope_studio/components/antd/steps/__init__.py +++ b/backend/modelscope_studio/components/antd/steps/__init__.py @@ -32,6 +32,7 @@ def __init__( label_placement: Literal['horizontal', 'vertical'] | None = None, title_placement: Literal['horizontal', 'vertical'] | None = None, percent: int | None = None, + max_count: int | None = None, progress_dot: bool | str = False, responsive: bool = True, size: Literal['small', 'default'] | None = None, @@ -66,6 +67,7 @@ def __init__( self.label_placement = label_placement self.title_placement = title_placement self.percent = percent + self.max_count = max_count self.progress_dot = progress_dot self.responsive = responsive self.size = size diff --git a/backend/modelscope_studio/components/antd/table/__init__.py b/backend/modelscope_studio/components/antd/table/__init__.py index 7b950a86..7826ac21 100644 --- a/backend/modelscope_studio/components/antd/table/__init__.py +++ b/backend/modelscope_studio/components/antd/table/__init__.py @@ -44,11 +44,12 @@ class AntdTable(ModelScopeLayoutComponent): 'title', 'summary', "expandable", - "rowSelection" + "rowSelection", 'loading.tip', 'loading.indicator', 'pagination.showQuickJumper.goButton', 'pagination.itemRender', + 'pagination.components.sizeChanger', 'showSorterTooltip.title', ] diff --git a/backend/modelscope_studio/components/antd/tabs/__init__.py b/backend/modelscope_studio/components/antd/tabs/__init__.py index 8c7f8e8c..8cbe988f 100644 --- a/backend/modelscope_studio/components/antd/tabs/__init__.py +++ b/backend/modelscope_studio/components/antd/tabs/__init__.py @@ -32,7 +32,7 @@ class AntdTabs(ModelScopeLayoutComponent): SLOTS = [ 'addIcon', 'removeIcon', 'renderTabBar', 'tabBarExtraContent', 'tabBarExtraContent.left', 'tabBarExtraContent.right', 'more.icon', - 'items' + 'more.popupRender', 'items' ] def __init__( diff --git a/backend/modelscope_studio/components/antd/time_picker/__init__.py b/backend/modelscope_studio/components/antd/time_picker/__init__.py index 9447894f..bc374c33 100644 --- a/backend/modelscope_studio/components/antd/time_picker/__init__.py +++ b/backend/modelscope_studio/components/antd/time_picker/__init__.py @@ -25,6 +25,9 @@ class AntdTimePicker(ModelScopeDataLayoutComponent): EventListener("calendar_change", callback=lambda block: block._internal.update( bind_calendarChange_event=True)), + EventListener("clear", + callback=lambda block: block._internal.update( + bind_clear_event=True)), ] # supported slots diff --git a/backend/modelscope_studio/components/antd/time_picker/range_picker/__init__.py b/backend/modelscope_studio/components/antd/time_picker/range_picker/__init__.py index 1eabb16b..dba715f0 100644 --- a/backend/modelscope_studio/components/antd/time_picker/range_picker/__init__.py +++ b/backend/modelscope_studio/components/antd/time_picker/range_picker/__init__.py @@ -26,6 +26,9 @@ class AntdTimePickerRangePicker(ModelScopeDataLayoutComponent): EventListener("blur", callback=lambda block: block._internal.update( bind_blur_event=True)), + EventListener("clear", + callback=lambda block: block._internal.update( + bind_clear_event=True)), ] # supported slots diff --git a/backend/modelscope_studio/components/antd/watermark/__init__.py b/backend/modelscope_studio/components/antd/watermark/__init__.py index 815d5c0d..4028a652 100644 --- a/backend/modelscope_studio/components/antd/watermark/__init__.py +++ b/backend/modelscope_studio/components/antd/watermark/__init__.py @@ -17,7 +17,7 @@ class AntdWatermark(ModelScopeLayoutComponent): def __init__( self, - content: str | list[str] | None = "", + content: str | dict | list[str | dict] | None = "", additional_props: dict | None = None, *, width: int | float | None = None, diff --git a/backend/modelscope_studio/components/antdx/folder/__init__.py b/backend/modelscope_studio/components/antdx/folder/__init__.py index f36c59fd..d06a9bd3 100644 --- a/backend/modelscope_studio/components/antdx/folder/__init__.py +++ b/backend/modelscope_studio/components/antdx/folder/__init__.py @@ -32,6 +32,9 @@ class AntdXFolder(ModelScopeLayoutComponent): EventListener("file_content_service_load_file_content", callback=lambda block: block._internal.update( bind_fileContentService_loadFileContent_event=True)), + EventListener("right_click", + callback=lambda block: block._internal.update( + bind_rightClick_event=True)), ] # supported slots @@ -57,6 +60,7 @@ def __init__( directory_title: str | None = None, preview_title: str | None = None, directory_icons: dict | None = None, + context_menu: list[dict] | str | None = None, root_class_name: str | None = None, class_names: dict | str | None = None, styles: dict | str | None = None, @@ -93,6 +97,7 @@ def __init__( self.directory_title = directory_title self.preview_title = preview_title self.directory_icons = directory_icons + self.context_menu = context_menu FRONTEND_DIR = resolve_frontend_dir("folder", type="antdx") diff --git a/backend/modelscope_studio/components/antdx/folder/tree_node/__init__.py b/backend/modelscope_studio/components/antdx/folder/tree_node/__init__.py index d95c2a04..95661197 100644 --- a/backend/modelscope_studio/components/antdx/folder/tree_node/__init__.py +++ b/backend/modelscope_studio/components/antdx/folder/tree_node/__init__.py @@ -21,6 +21,7 @@ def __init__( title: str | None = None, path: str | None = None, content: str | None = None, + context_menu: list[dict] | bool | None = None, additional_props: dict | None = None, as_item: str | None = None, _internal: None = None, @@ -42,6 +43,7 @@ def __init__( self.additional_props = additional_props self.path = path self.content = content + self.context_menu = context_menu FRONTEND_DIR = resolve_frontend_dir("folder", "tree-node", type="antdx") diff --git a/backend/modelscope_studio/components/antdx/think/__init__.py b/backend/modelscope_studio/components/antdx/think/__init__.py index 25ad92cf..03719cae 100644 --- a/backend/modelscope_studio/components/antdx/think/__init__.py +++ b/backend/modelscope_studio/components/antdx/think/__init__.py @@ -31,6 +31,7 @@ def __init__( default_expanded: bool | None = None, expanded: bool | None = None, blink: bool | None = None, + destroy_on_hidden: bool | None = None, as_item: str | None = None, _internal: None = None, # gradio properties @@ -55,6 +56,7 @@ def __init__( self.default_expanded = default_expanded self.expanded = expanded self.blink = blink + self.destroy_on_hidden = destroy_on_hidden self.title = title self.root_class_name = root_class_name diff --git a/backend/modelscope_studio/components/antdx/thought_chain/thought_chain_item/__init__.py b/backend/modelscope_studio/components/antdx/thought_chain/thought_chain_item/__init__.py index 3e50fd7c..1609c8ff 100644 --- a/backend/modelscope_studio/components/antdx/thought_chain/thought_chain_item/__init__.py +++ b/backend/modelscope_studio/components/antdx/thought_chain/thought_chain_item/__init__.py @@ -30,6 +30,7 @@ def __init__( | None = None, collapsible: bool | None = None, blink: bool | None = None, + destroy_on_hidden: bool | None = None, as_item: str | None = None, _internal: None = None, # gradio properties @@ -57,6 +58,7 @@ def __init__( self.key = key self.collapsible = collapsible self.blink = blink + self.destroy_on_hidden = destroy_on_hidden FRONTEND_DIR = resolve_frontend_dir("thought-chain", "thought-chain-item", diff --git a/config/changelog/package.json b/config/changelog/package.json index 2808cbad..effc90ba 100644 --- a/config/changelog/package.json +++ b/config/changelog/package.json @@ -19,13 +19,13 @@ "build": "tsup" }, "dependencies": { - "@changesets/get-github-info": "^0.8.0", + "@changesets/get-github-info": "^1.0.0", "@manypkg/get-packages": "^3.1.0", "detect-indent": "^7.0.2" }, "devDependencies": { - "@changesets/types": "^6.1.0", - "@types/node": "^25.8.0", + "@changesets/types": "^7.0.0", + "@types/node": "^26.2.0", "tsup": "^8.5.1" } } diff --git a/config/changelog/src/index.ts b/config/changelog/src/index.ts index bfca6376..7f53acf5 100644 --- a/config/changelog/src/index.ts +++ b/config/changelog/src/index.ts @@ -1,4 +1,4 @@ -import { getInfo, getInfoFromPullRequest } from '@changesets/get-github-info'; +import { getCommitInfo, getPullRequestInfo } from '@changesets/get-github-info'; import type { ChangelogFunctions } from '@changesets/types'; import { getPackagesSync } from '@manypkg/get-packages'; import { existsSync, readFileSync, writeFileSync } from 'fs'; @@ -15,6 +15,25 @@ function find_packages_dir(package_name: string) { return _package.dir; } +/** + * `changelogOpts` is typed as `null | Record`, so the repo has + * to be both null checked and narrowed to a string before it can be used. + * + * @param {null | Record} options The changelog options + * @returns {string} The `org/repo` slug + */ +function get_repo(options: null | Record): string { + const repo = options?.repo; + + if (typeof repo !== 'string' || !repo) { + throw new Error( + 'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]' + ); + } + + return repo; +} + /** * @param {string} str The changelog entry * @param {string} prefix The prefix to add to the first line @@ -42,22 +61,19 @@ const changelogFunctions: ChangelogFunctions = { dependenciesUpdated, options ) => { - if (!options.repo) { - throw new Error( - 'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]' - ); - } + const repo = get_repo(options); + if (dependenciesUpdated.length === 0) return ''; const changesetLink = `- Updated dependencies [${( await Promise.all( changesets.map(async (cs) => { if (cs.commit) { - const { links } = await getInfo({ - repo: options.repo, + const info = await getCommitInfo({ + repo, commit: cs.commit, }); - return links.commit; + return info?.commit.markdownLink; } }) ) @@ -72,11 +88,7 @@ const changelogFunctions: ChangelogFunctions = { return [changesetLink, ...updatedDepenenciesList].join('\n'); }, getReleaseLine: async (changeset, _type, options) => { - if (!options || !options.repo) { - throw new Error( - 'Please provide a repo to this changelog generator like this:\n"changelog": ["@changesets/changelog-github", { "repo": "org/repo" }]' - ); - } + const repo = get_repo(options); let prFromSummary: number | undefined; let commitFromSummary: string | undefined; @@ -102,28 +114,38 @@ const changelogFunctions: ChangelogFunctions = { .split('\n') .map((l) => l.trimEnd()); - const links = await (async () => { + const links: { + commit: string | null; + pull: string | null; + user: string | null; + } = await (async () => { if (prFromSummary !== undefined) { - let { links: _links } = await getInfoFromPullRequest({ - repo: options.repo, + const info = await getPullRequestInfo({ + repo, pull: prFromSummary, }); - if (commitFromSummary) { - const shortCommitId = commitFromSummary.slice(0, 7); - _links = { - ..._links, - commit: `[\`${shortCommitId}\`](https://github.com/${options.repo}/commit/${commitFromSummary})`, - }; - } - return _links; + return { + commit: commitFromSummary + ? `[\`${commitFromSummary.slice( + 0, + 7 + )}\`](https://github.com/${repo}/commit/${commitFromSummary})` + : (info?.commit?.markdownLink ?? null), + pull: info?.pull.markdownLink ?? null, + user: info?.author?.markdownLink ?? null, + }; } const commitToFetchFrom = commitFromSummary || changeset.commit; if (commitToFetchFrom) { - const info = await getInfo({ - repo: options.repo, + const info = await getCommitInfo({ + repo, commit: commitToFetchFrom, }); - return info.links; + return { + commit: info?.commit.markdownLink ?? null, + pull: info?.pull?.markdownLink ?? null, + user: info?.author?.markdownLink ?? null, + }; } return { commit: null, diff --git a/config/lint-config/package.json b/config/lint-config/package.json index 9fcdf896..1a62c23f 100644 --- a/config/lint-config/package.json +++ b/config/lint-config/package.json @@ -18,27 +18,27 @@ "dependencies": { "@eslint/compat": "^2.1.0", "@eslint/js": "^10.0.1", - "@typescript-eslint/parser": "^8.59.3", + "@typescript-eslint/parser": "^8.67.0", "eslint-config-prettier": "^10.1.8", - "eslint-import-resolver-typescript": "^4.4.4", + "eslint-import-resolver-typescript": "^4.4.5", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-prettier": "5.5.5", + "eslint-plugin-prettier": "5.5.6", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.2", - "eslint-plugin-simple-import-sort": "^13.0.0", - "eslint-plugin-svelte": "^3.17.1", - "globals": "^17.6.0", - "postcss": "^8.5.14", + "eslint-plugin-react-refresh": "^0.5.4", + "eslint-plugin-simple-import-sort": "^14.0.0", + "eslint-plugin-svelte": "^3.23.0", + "globals": "^17.11.0", + "postcss": "^8.5.26", "postcss-less": "^6.0.0", "stylelint-config-rational-order": "^0.1.2", "stylelint-config-standard": "^40.0.0", "stylelint-declaration-block-no-ignored-properties": "^3.0.0", "stylelint-order": "^8.1.1", "stylelint-prettier": "^5.0.3", - "svelte-eslint-parser": "^1.6.1", - "typescript-eslint": "^8.59.3" + "svelte-eslint-parser": "^1.8.0", + "typescript-eslint": "^8.67.0" }, "devDependencies": { "@types/eslint": "^9.6.1", diff --git a/docs/app.py b/docs/app.py index 68fa1e0f..ac97adc2 100644 --- a/docs/app.py +++ b/docs/app.py @@ -353,6 +353,9 @@ def render(self): }, { "label": get_text("List", "List 列表"), "key": "list" + }, { + "label": get_text("Listy", "Listy 虚拟列表"), + "key": "listy" }, { "label": get_text("Popover", "Popover 气泡卡片"), "key": "popover" @@ -434,6 +437,9 @@ def render(self): "children": [{ "label": get_text("Affix", "Affix 固钉"), "key": "affix" + }, { + "label": get_text("BorderBeam", "BorderBeam 边框流光"), + "key": "border_beam" }, { "label": get_text("ConfigProvider", "ConfigProvider 全局化配置"), "key": "config_provider" diff --git a/docs/components/antd/border_beam/README-zh_CN.md b/docs/components/antd/border_beam/README-zh_CN.md new file mode 100644 index 00000000..c69f16fa --- /dev/null +++ b/docs/components/antd/border_beam/README-zh_CN.md @@ -0,0 +1,9 @@ +# BorderBeam + +A decorative component that renders a flowing highlight animation along the border of its child container. See [Ant Design](https://ant.design/components/border-beam/) for more information. + +## Examples + + + + diff --git a/docs/components/antd/border_beam/README.md b/docs/components/antd/border_beam/README.md new file mode 100644 index 00000000..c69f16fa --- /dev/null +++ b/docs/components/antd/border_beam/README.md @@ -0,0 +1,9 @@ +# BorderBeam + +A decorative component that renders a flowing highlight animation along the border of its child container. See [Ant Design](https://ant.design/components/border-beam/) for more information. + +## Examples + + + + diff --git a/docs/components/antd/border_beam/app.py b/docs/components/antd/border_beam/app.py new file mode 100644 index 00000000..0018b7c1 --- /dev/null +++ b/docs/components/antd/border_beam/app.py @@ -0,0 +1,6 @@ +from helper.Docs import Docs + +docs = Docs(__file__) + +if __name__ == "__main__": + docs.render().queue().launch() diff --git a/docs/components/antd/border_beam/demos/basic.py b/docs/components/antd/border_beam/demos/basic.py new file mode 100644 index 00000000..85c90cd4 --- /dev/null +++ b/docs/components/antd/border_beam/demos/basic.py @@ -0,0 +1,14 @@ +import gradio as gr +import modelscope_studio.components.antd as antd +import modelscope_studio.components.base as ms + +with gr.Blocks() as demo: + with ms.Application(): + with antd.ConfigProvider(): + with antd.BorderBeam(elem_style=dict(borderRadius=8)): + with antd.Card(title="BorderBeam", + elem_style=dict(width=320, marginBottom=0)): + ms.Div("A flowing highlight runs along the border.") + +if __name__ == "__main__": + demo.queue().launch() diff --git a/docs/components/antd/border_beam/demos/color.py b/docs/components/antd/border_beam/demos/color.py new file mode 100644 index 00000000..f58a9298 --- /dev/null +++ b/docs/components/antd/border_beam/demos/color.py @@ -0,0 +1,30 @@ +import gradio as gr +import modelscope_studio.components.antd as antd +import modelscope_studio.components.base as ms + +with gr.Blocks() as demo: + with ms.Application(): + with antd.ConfigProvider(): + with antd.Space(size="large"): + # a solid color beam + with antd.BorderBeam(color="#f5222d", + duration=3, + elem_style=dict(borderRadius=8)): + with antd.Card(title="Solid Color", + elem_style=dict(width=280, marginBottom=0)): + ms.Div("color='#f5222d'") + # a gradient beam + with antd.BorderBeam(color=[ + dict(color="#1677ff", percent=0), + dict(color="#722ed1", percent=50), + dict(color="#eb2f96", percent=70), + ], + line_width=2, + size=120, + elem_style=dict(borderRadius=8)): + with antd.Card(title="Gradient", + elem_style=dict(width=280, marginBottom=0)): + ms.Div("color=[{color, percent}, ...]") + +if __name__ == "__main__": + demo.queue().launch() diff --git a/docs/components/antd/border_beam/demos/multiple_beams.py b/docs/components/antd/border_beam/demos/multiple_beams.py new file mode 100644 index 00000000..75a96501 --- /dev/null +++ b/docs/components/antd/border_beam/demos/multiple_beams.py @@ -0,0 +1,21 @@ +import gradio as gr +import modelscope_studio.components.antd as antd +import modelscope_studio.components.base as ms + +with gr.Blocks() as demo: + with ms.Application(): + with antd.ConfigProvider(): + with antd.Space(size="large"): + for count in [1, 2, 4]: + with antd.BorderBeam(count=count, + duration=4, + elem_style=dict(borderRadius=8)): + with antd.Card(title=f"count={count}", + elem_style=dict(width=240, + marginBottom=0)): + ms.Div( + f"{count} beam(s) evenly distributed along the border." + ) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/docs/components/antd/listy/README-zh_CN.md b/docs/components/antd/listy/README-zh_CN.md new file mode 100644 index 00000000..a9bf714c --- /dev/null +++ b/docs/components/antd/listy/README-zh_CN.md @@ -0,0 +1,11 @@ +# Listy + +A high performance list component which renders large data sets with virtual scrolling. See [Ant Design](https://ant.design/components/listy/) for more information. + +Items are plain data, and each item is rendered by the `itemRender` slot (or the `item_render` function property). `row_key` accepts either a field name of the item or a Javascript function string. + +## Examples + + + + diff --git a/docs/components/antd/listy/README.md b/docs/components/antd/listy/README.md new file mode 100644 index 00000000..a9bf714c --- /dev/null +++ b/docs/components/antd/listy/README.md @@ -0,0 +1,11 @@ +# Listy + +A high performance list component which renders large data sets with virtual scrolling. See [Ant Design](https://ant.design/components/listy/) for more information. + +Items are plain data, and each item is rendered by the `itemRender` slot (or the `item_render` function property). `row_key` accepts either a field name of the item or a Javascript function string. + +## Examples + + + + diff --git a/docs/components/antd/listy/app.py b/docs/components/antd/listy/app.py new file mode 100644 index 00000000..0018b7c1 --- /dev/null +++ b/docs/components/antd/listy/app.py @@ -0,0 +1,6 @@ +from helper.Docs import Docs + +docs = Docs(__file__) + +if __name__ == "__main__": + docs.render().queue().launch() diff --git a/docs/components/antd/listy/demos/basic.py b/docs/components/antd/listy/demos/basic.py new file mode 100644 index 00000000..33849fd4 --- /dev/null +++ b/docs/components/antd/listy/demos/basic.py @@ -0,0 +1,25 @@ +import gradio as gr +import modelscope_studio.components.antd as antd +import modelscope_studio.components.base as ms + +items = [{ + "key": f"item-{i}", + "title": f"Item {i}", +} for i in range(2000)] + +with gr.Blocks() as demo: + with ms.Application(): + with antd.ConfigProvider(): + with antd.Listy(items=items, + row_key="key", + height=360, + virtual=True, + elem_style=dict(border="1px solid #f0f0f0", + borderRadius=8)): + with ms.Slot( + "itemRender", + params_mapping="(item) => ({ value: item.title })"): + ms.Div(elem_style=dict(padding="0 16px")) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/docs/components/antd/listy/demos/group.py b/docs/components/antd/listy/demos/group.py new file mode 100644 index 00000000..1b4a0c7d --- /dev/null +++ b/docs/components/antd/listy/demos/group.py @@ -0,0 +1,36 @@ +import gradio as gr +import modelscope_studio.components.antd as antd +import modelscope_studio.components.base as ms + +categories = ["Fruit", "Vegetable", "Drink", "Snack"] + +items = [{ + "key": f"item-{i}", + "title": f"Item {i}", + "category": categories[i % len(categories)], +} for i in range(400)] + +with gr.Blocks() as demo: + with ms.Application(): + with antd.ConfigProvider(): + with antd.Listy(items=items, + row_key="key", + height=360, + sticky=True, + virtual=True, + group=dict(key="(item) => item.category"), + elem_style=dict(border="1px solid #f0f0f0", + borderRadius=8)): + with ms.Slot( + "group.title", + params_mapping= + "(groupKey, items) => ({ value: groupKey + ' (' + items.length + ')' })" + ): + ms.Div(elem_style=dict(fontWeight=600)) + with ms.Slot( + "itemRender", + params_mapping="(item) => ({ value: item.title })"): + ms.Div(elem_style=dict(padding="0 16px")) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/docs/components/antd/listy/demos/item_render_function.py b/docs/components/antd/listy/demos/item_render_function.py new file mode 100644 index 00000000..ec861e11 --- /dev/null +++ b/docs/components/antd/listy/demos/item_render_function.py @@ -0,0 +1,23 @@ +import gradio as gr +import modelscope_studio.components.antd as antd +import modelscope_studio.components.base as ms + +items = [{ + "id": i, + "title": f"Item {i}", +} for i in range(1000)] + +with gr.Blocks() as demo: + with ms.Application(): + with antd.ConfigProvider(): + # both `row_key` and `item_render` accept a Javascript function string + antd.Listy( + items=items, + row_key="(item) => item.id", + item_render="(item, index) => `#${index} ${item.title}`", + height=360, + virtual=True, + elem_style=dict(border="1px solid #f0f0f0", borderRadius=8)) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/docs/components/antdx/folder/README-zh_CN.md b/docs/components/antdx/folder/README-zh_CN.md index dd33337c..6c5766d2 100644 --- a/docs/components/antdx/folder/README-zh_CN.md +++ b/docs/components/antdx/folder/README-zh_CN.md @@ -5,3 +5,4 @@ A folder tree component for displaying file structures. See [Ant Design X](https ## Examples + diff --git a/docs/components/antdx/folder/README.md b/docs/components/antdx/folder/README.md index dd33337c..6c5766d2 100644 --- a/docs/components/antdx/folder/README.md +++ b/docs/components/antdx/folder/README.md @@ -5,3 +5,4 @@ A folder tree component for displaying file structures. See [Ant Design X](https ## Examples + diff --git a/docs/components/antdx/folder/demos/context_menu.py b/docs/components/antdx/folder/demos/context_menu.py new file mode 100644 index 00000000..d13c59fa --- /dev/null +++ b/docs/components/antdx/folder/demos/context_menu.py @@ -0,0 +1,67 @@ +import gradio as gr +import modelscope_studio.components.antd as antd +import modelscope_studio.components.antdx as antdx +import modelscope_studio.components.base as ms + + +def on_right_click(e: gr.EventData): + print("right clicked:", e._data["payload"]) + + +with gr.Blocks() as demo: + with ms.Application(): + with antdx.XProvider(): + antd.Divider("Shared Context Menu") + # `context_menu` applies to all nodes, a node can override it + # with its own `context_menu` (pass False to disable). + with antdx.Folder(default_expand_all=True, + context_menu=[{ + "key": "rename", + "label": "Rename" + }, { + "key": "delete", + "label": "Delete", + "danger": True + }]) as folder: + with antdx.Folder.TreeNode(title="src", path="/src"): + antdx.Folder.TreeNode(title="index.ts", + path="/src/index.ts", + content="export * from './app';") + # disable the context menu for this node only + antdx.Folder.TreeNode(title="app.ts", + path="/src/app.ts", + content="console.log('Hello');", + context_menu=False) + antdx.Folder.TreeNode( + title="package.json", + path="/package.json", + content='{\n "name": "my-project"\n}', + # override the shared menu for this node + context_menu=[{ + "key": "copy_path", + "label": "Copy Path" + }]) + + antd.Divider("Dynamic Context Menu") + # `context_menu` also accepts a Javascript function string, + # which receives the node data and its full path key. + antdx.Folder( + default_expand_all=True, + context_menu= + "(node, key) => node.children ? [{ key: 'new_file', label: 'New File' }] : [{ key: 'open', label: 'Open ' + key }]", + tree_data=[{ + "title": + "project", + "path": + "/project", + "children": [{ + "title": "main.py", + "path": "/project/main.py", + "content": "print('Hello, World!')" + }] + }]) + + folder.right_click(fn=on_right_click) + +if __name__ == "__main__": + demo.queue().launch() diff --git a/eslint.config.mjs b/eslint.config.mjs index 013a9444..c0352941 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,6 +3,11 @@ import baseConfig from '@modelscope-studio/lint-config/eslint'; /** * @type {import('eslint').Linter.Config[]} */ -const config = []; +const config = [ + { + // Vendored `@gradio/preview`, kept byte-for-byte in sync with upstream. + ignores: ['patches/**'], + }, +]; export default config.concat(baseConfig); diff --git a/frontend/antd/border-beam/Index.svelte b/frontend/antd/border-beam/Index.svelte new file mode 100644 index 00000000..d7f44820 --- /dev/null +++ b/frontend/antd/border-beam/Index.svelte @@ -0,0 +1,64 @@ + + +{#if proceedProps.visible} + {#await AwaitedBorderBeam then BorderBeam} + + {@render children?.()} + + {/await} +{/if} + + diff --git a/frontend/antd/border-beam/border-beam.tsx b/frontend/antd/border-beam/border-beam.tsx new file mode 100644 index 00000000..c0e0e958 --- /dev/null +++ b/frontend/antd/border-beam/border-beam.tsx @@ -0,0 +1,32 @@ +import { sveltify } from '@svelte-preprocess-react'; +import React from 'react'; +import { BorderBeam as ABorderBeam, type GetProps } from 'antd'; + +export const BorderBeam = sveltify< + GetProps & { + children?: React.ReactNode; + } +>(({ children, className, style, id, ...props }) => { + return ( + + {/* + antd resolves the beam host by cloning its single child with a ref, but + Svelte layout children arrive as bridge elements that expose no DOM + node, so `useChildDom` would bail out and no beam would be rendered at + all. Render the host ourselves instead: it has to be positioned because + the beam is absolutely positioned inside it, and antd reads the beam + border width and radius off its computed style, so `borderRadius` has to + be set through `elem_style` to match the wrapped content. + */} +
+ {children} +
+
+ ); +}); + +export default BorderBeam; diff --git a/frontend/antd/border-beam/gradio.config.js b/frontend/antd/border-beam/gradio.config.js new file mode 100644 index 00000000..916857c0 --- /dev/null +++ b/frontend/antd/border-beam/gradio.config.js @@ -0,0 +1,3 @@ +import config from '../../defineConfig.js'; + +export default config(); diff --git a/frontend/antd/border-beam/package.json b/frontend/antd/border-beam/package.json new file mode 100644 index 00000000..1b33be32 --- /dev/null +++ b/frontend/antd/border-beam/package.json @@ -0,0 +1,14 @@ +{ + "name": "@modelscope-studio/antd-border-beam", + "type": "module", + "exports": { + ".": { + "gradio": "./Index.svelte", + "default": "./Index.svelte" + }, + "./package.json": { + "gradio": "./package.json", + "default": "./package.json" + } + } +} diff --git a/frontend/antd/config-provider/locales.ts b/frontend/antd/config-provider/locales.ts index ba9a20c4..f9e1eee0 100644 --- a/frontend/antd/config-provider/locales.ts +++ b/frontend/antd/config-provider/locales.ts @@ -70,6 +70,7 @@ export const lang2RegionMap = { si: 'si_LK', // Sinhala sk: 'sk_SK', sl: 'sl_SI', + sq: 'sq_AL', // Albanian sr: 'sr_RS', // Serbian sv: 'sv_SE', ta: 'ta_IN', // Tamil @@ -717,6 +718,17 @@ export const locales: Record< }; }, + sq_AL: async () => { + const [{ default: antd }] = await Promise.all([ + import('antd/locale/sq_AL'), + import('dayjs/locale/sq'), // Albanian + ]); + return { + antd, + dayjs: 'sq', + }; + }, + sr_RS: async () => { const [{ default: antd }] = await Promise.all([ import('antd/locale/sr_RS'), diff --git a/frontend/antd/listy/Index.svelte b/frontend/antd/listy/Index.svelte new file mode 100644 index 00000000..b9cbe0fa --- /dev/null +++ b/frontend/antd/listy/Index.svelte @@ -0,0 +1,64 @@ + + +{#if proceedProps.visible} + {#await AwaitedListy then Listy} + + {@render children?.()} + + {/await} +{/if} + + diff --git a/frontend/antd/listy/gradio.config.js b/frontend/antd/listy/gradio.config.js new file mode 100644 index 00000000..916857c0 --- /dev/null +++ b/frontend/antd/listy/gradio.config.js @@ -0,0 +1,3 @@ +import config from '../../defineConfig.js'; + +export default config(); diff --git a/frontend/antd/listy/listy.tsx b/frontend/antd/listy/listy.tsx new file mode 100644 index 00000000..8475eb32 --- /dev/null +++ b/frontend/antd/listy/listy.tsx @@ -0,0 +1,57 @@ +import { sveltify } from '@svelte-preprocess-react'; +import React, { useMemo } from 'react'; +import { useFunction } from '@utils/hooks/useFunction'; +import { renderParamsSlot } from '@utils/renderParamsSlot'; +import { type GetProps, Listy as AListy } from 'antd'; + +type ListyProps = GetProps; +type ListyGroup = NonNullable; + +export const Listy = sveltify< + Omit & { + rowKey?: ListyProps['rowKey']; + itemRender?: ListyProps['itemRender'] | string; + group?: { + key?: ListyGroup['key'] | string; + title?: ListyGroup['title'] | string; + }; + children?: React.ReactNode; + }, + ['itemRender', 'group.title'] +>(({ children, slots, group, rowKey, itemRender, ...props }) => { + const rowKeyFunction = useFunction(rowKey, true); + const itemRenderFunction = useFunction(itemRender); + const groupKeyFunction = useFunction(group?.key); + const groupTitleFunction = useFunction(group?.title); + const itemRenderSlot = slots.itemRender; + const groupTitleSlot = slots['group.title']; + const mergedGroup = useMemo(() => { + if (!group) { + return undefined; + } + return { + ...group, + key: groupKeyFunction || group.key, + title: groupTitleSlot + ? renderParamsSlot({ slots, key: 'group.title' }) + : groupTitleFunction || group.title, + } as ListyProps['group']; + }, [group, groupKeyFunction, groupTitleFunction, groupTitleSlot, slots]); + return ( + <> +
{children}
+ + + ); +}); + +export default Listy; diff --git a/frontend/antd/listy/package.json b/frontend/antd/listy/package.json new file mode 100644 index 00000000..9b0edfa2 --- /dev/null +++ b/frontend/antd/listy/package.json @@ -0,0 +1,14 @@ +{ + "name": "@modelscope-studio/antd-listy", + "type": "module", + "exports": { + ".": { + "gradio": "./Index.svelte", + "default": "./Index.svelte" + }, + "./package.json": { + "gradio": "./package.json", + "default": "./package.json" + } + } +} diff --git a/frontend/antd/mentions/mentions.tsx b/frontend/antd/mentions/mentions.tsx index e5bcd230..c6cee72d 100644 --- a/frontend/antd/mentions/mentions.tsx +++ b/frontend/antd/mentions/mentions.tsx @@ -4,6 +4,7 @@ import { useMemo } from 'react'; import { useFunction } from '@utils/hooks/useFunction'; import { useValueChange } from '@utils/hooks/useValueChange'; import { renderItems } from '@utils/renderItems'; +import { renderParamsSlot } from '@utils/renderParamsSlot'; import { type GetProps, Mentions as AMentions } from 'antd'; import { useItems, withItemsContextProvider } from './context'; @@ -12,7 +13,7 @@ export const Mentions = sveltify< GetProps & { onValueChange: (value: string) => void; }, - ['allowClear.clearIcon', 'notFoundContent'] + ['allowClear.clearIcon', 'notFoundContent', 'popupRender'] >( withItemsContextProvider( ['options', 'default'], @@ -25,12 +26,14 @@ export const Mentions = sveltify< options, validateSearch, getPopupContainer, + popupRender, elRef, ...props }) => { const getPopupContainerFunction = useFunction(getPopupContainer); const filterOptionFunction = useFunction(filterOption); const validateSearchFunction = useFunction(validateSearch); + const popupRenderFunction = useFunction(popupRender); const [value, setValue] = useValueChange({ onValueChange, value: props.value, @@ -69,6 +72,11 @@ export const Mentions = sveltify< } filterOption={filterOptionFunction || filterOption} getPopupContainer={getPopupContainerFunction} + popupRender={ + slots.popupRender + ? renderParamsSlot({ slots, key: 'popupRender' }) + : popupRenderFunction + } /> ); diff --git a/frontend/antd/pagination/pagination.tsx b/frontend/antd/pagination/pagination.tsx index 5d83e653..6db79424 100644 --- a/frontend/antd/pagination/pagination.tsx +++ b/frontend/antd/pagination/pagination.tsx @@ -9,7 +9,7 @@ export const Pagination = sveltify< GetProps & { children?: React.ReactNode; }, - ['showQuickJumper.goButton', 'itemRender'] + ['showQuickJumper.goButton', 'itemRender', 'components.sizeChanger'] >( ({ slots, @@ -18,6 +18,7 @@ export const Pagination = sveltify< onChange, children, itemRender, + components, ...props }) => { const itemRenderFunction = useFunction(itemRender); @@ -28,6 +29,17 @@ export const Pagination = sveltify< { @@ -54,6 +55,7 @@ export const Select = sveltify< const optionRenderFunction = useFunction(optionRender); const tagRenderFunction = useFunction(tagRender); const labelRenderFunction = useFunction(labelRender); + const tokenSeparatorsFunction = useFunction(tokenSeparators); const { items: slotItems } = useItems<['options', 'default']>(); const resolvedSlotItems = slotItems.options.length > 0 ? slotItems.options : slotItems.default; @@ -120,6 +122,7 @@ export const Select = sveltify< ) } filterOption={filterOptionFunction || filterOption} + tokenSeparators={tokenSeparatorsFunction || tokenSeparators} maxTagPlaceholder={ slots.maxTagPlaceholder ? renderParamsSlot({ diff --git a/frontend/antd/table/table.tsx b/frontend/antd/table/table.tsx index ae55eb9a..732c201b 100644 --- a/frontend/antd/table/table.tsx +++ b/frontend/antd/table/table.tsx @@ -34,6 +34,7 @@ export const Table = sveltify< 'loading.indicator', 'pagination.showQuickJumper.goButton', 'pagination.itemRender', + 'pagination.components.sizeChanger', 'showSorterTooltip.title', 'summary', ] @@ -80,7 +81,8 @@ export const Table = sveltify< const loadingConfig = getConfig(loading); const supportPaginationConfig = slots['pagination.showQuickJumper.goButton'] || - slots['pagination.itemRender']; + slots['pagination.itemRender'] || + slots['pagination.components.sizeChanger']; const paginationConfig = getConfig(pagination); const paginationShowTotalFunction = useFunction( paginationConfig.showTotal @@ -235,6 +237,15 @@ export const Table = sveltify< key: 'pagination.itemRender', }) : paginationConfig.itemRender, + components: slots['pagination.components.sizeChanger'] + ? { + ...paginationConfig.components, + sizeChanger: renderParamsSlot({ + slots, + key: 'pagination.components.sizeChanger', + }), + } + : paginationConfig.components, }) : pagination } diff --git a/frontend/antd/tabs/tabs.tsx b/frontend/antd/tabs/tabs.tsx index 82db68ad..52e41cc8 100644 --- a/frontend/antd/tabs/tabs.tsx +++ b/frontend/antd/tabs/tabs.tsx @@ -19,6 +19,7 @@ export const Tabs = sveltify< 'tabBarExtraContent.left', 'tabBarExtraContent.right', 'more.icon', + 'more.popupRender', ] >( withItemsContextProvider( @@ -37,6 +38,7 @@ export const Tabs = sveltify< const getMorePopupContainerFunction = useFunction( more?.getPopupContainer ); + const morePopupRenderFunction = useFunction(more?.popupRender); const renderTabBarFunction = useFunction(renderTabBar); const { items: slotItems } = useItems<['default', 'items']>(); const resolvedSlotItems = @@ -74,6 +76,12 @@ export const Tabs = sveltify< ...(more || {}), getPopupContainer: getMorePopupContainerFunction || more?.getPopupContainer, + popupRender: slots['more.popupRender'] + ? renderParamsSlot({ + slots, + key: 'more.popupRender', + }) + : morePopupRenderFunction || more?.popupRender, icon: slots['more.icon'] ? ( ) : ( diff --git a/frontend/antdx/folder/folder.tsx b/frontend/antdx/folder/folder.tsx index 825cc8b8..30d39ef1 100644 --- a/frontend/antdx/folder/folder.tsx +++ b/frontend/antdx/folder/folder.tsx @@ -32,6 +32,7 @@ export const Folder = sveltify< previewRender, previewTitle, fileContentService, + contextMenu, ...props }) => { const { items: treeNodeItems } = @@ -44,12 +45,14 @@ export const Folder = sveltify< useDirectoryIconItems<['directoryIcons']>(); const previewRenderFunction = useFunction(previewRender, true); const previewTitleFunction = useFunction(previewTitle, true); + const contextMenuFunction = useFunction(contextMenu, true); return ( <>
{children}
{ let loadingContent: - | React.ReactNode - | ((...args: any[]) => React.ReactNode) = null; + React.ReactNode | ((...args: any[]) => React.ReactNode) = null; let errorContent: React.ReactNode | ((...args: any[]) => React.ReactNode) = null; const { diff --git a/frontend/defineConfig.js b/frontend/defineConfig.js index c71fa52a..fc7f98a9 100644 --- a/frontend/defineConfig.js +++ b/frontend/defineConfig.js @@ -1,4 +1,4 @@ -import react from '@vitejs/plugin-react-swc'; +import react from '@vitejs/plugin-react'; import { ModelScopeStudioVitePlugin } from './plugin.js'; diff --git a/frontend/fixtures.d.ts b/frontend/fixtures.d.ts index 1be6f19c..a6dca3ab 100644 --- a/frontend/fixtures.d.ts +++ b/frontend/fixtures.d.ts @@ -28,6 +28,8 @@ declare module '*?raw' { declare module '*css' {} +declare module '*less' {} + interface Window { __gradio_space__: any; } diff --git a/frontend/package.json b/frontend/package.json index 947a6a39..970c7d18 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,52 +7,52 @@ "type": "module", "dependencies": { "@ant-design/cssinjs": "^2.1.2", - "@ant-design/icons": "^6.2.3", - "@ant-design/x": "^2.7.0", - "@babel/standalone": "^7.29.4", + "@ant-design/icons": "^6.3.2", + "@ant-design/x": "^2.9.0", + "@babel/standalone": "^8.0.4", "@gradio/client": "2.1.0", - "@gradio/preview": "0.15.2", + "@gradio/preview": "workspace:@modelscope-studio/gradio-preview@*", "@gradio/statustracker": "^0.13.1", "@gradio/utils": "0.11.3", "@monaco-editor/react": "^4.7.0", "amuchina": "^1.0.12", - "antd": "^6.4.2", + "antd": "^6.6.0", "classnames": "^2.5.1", - "dayjs": "^1.11.20", + "dayjs": "^1.11.21", "dequal": "^2.0.2", "github-slugger": "^2.0.0", - "immer": "^11.1.8", - "katex": "^0.16.47", + "immer": "^11.1.16", + "katex": "^0.18.4", "lodash-es": "^4.18.1", - "marked": "^18.0.3", + "marked": "^18.0.9", "marked-gfm-heading-id": "^4.1.4", "marked-highlight": "^2.2.4", - "mermaid": "^11.15.0", - "monaco-editor": "^0.55.1", + "mermaid": "^11.16.1", + "monaco-editor": "^0.56.0", "path-browserify-esm": "^1.0.6", "prismjs": "^1.30.0", - "react": "^19.2.6", - "react-dom": "^19.2.6", + "react": "^19.2.8", + "react-dom": "^19.2.8", "react-syntax-highlighter": "^16.1.1", - "svelte": "5.55.7", + "svelte": "5.48.0", "svelte-i18n": "^4.0.1", - "wavesurfer.js": "^7.12.7" + "wavesurfer.js": "^7.12.11" }, "devDependencies": { - "@babel/core": "^7.29.0", + "@babel/core": "^8.0.1", "@types/babel__core": "^7.20.5", "@types/babel__standalone": "^7.1.9", "@types/katex": "^0.16.8", "@types/lodash-es": "^4.17.12", "@types/prismjs": "^1.26.6", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "@types/react-syntax-highlighter": "^15.5.13", "@types/sanitize-html": "^2.16.1", - "@vitejs/plugin-react-swc": "^4.3.1", - "less": "^4.6.4", - "typescript-json-schema": "^0.67.3", - "vite": "^7.3.1" + "@vitejs/plugin-react": "^6.0.5", + "less": "^4.8.1", + "typescript-json-schema": "^0.68.0", + "vite": "^8.2.1" }, "main_changeset": true } diff --git a/frontend/plugin.js b/frontend/plugin.js index 3f6554c3..89b96b32 100644 --- a/frontend/plugin.js +++ b/frontend/plugin.js @@ -1,6 +1,12 @@ -import { parse, transformFromAstSync, traverse, types as t } from '@babel/core'; +import { + parseSync, + transformFromAstSync, + traverse, + types as t, +} from '@babel/core'; import path from 'node:path'; import url from 'node:url'; +import { esmExternalRequirePlugin } from 'vite'; // Each entry mirrors how the global is exposed in // `frontend/svelte-preprocess-react/inject.ts`: @@ -48,6 +54,31 @@ const baseGlobals = { const dirname = path.dirname(url.fileURLToPath(import.meta.url)); +/** + * `t.identifier` only accepts a valid identifier name, so a dotted global path + * like `window.ms_globals.React` has to be built as a member expression. + */ +function createGlobalExpression(variable) { + const [object, ...properties] = variable.split('.'); + return properties.reduce( + (expression, property) => + t.memberExpression(expression, t.identifier(property)), + t.identifier(object) + ); +} + +/** + * Access the imported/exported name on the global expression, string literal + * names (eg: `import { 'a-b' as c } from 'react'`) must be computed. + */ +function createGlobalMemberExpression(variable, property) { + return t.memberExpression( + createGlobalExpression(variable), + t.cloneNode(property), + t.isStringLiteral(property) + ); +} + function generateSveltePreprocessReactAliases() { const baseDir = 'svelte-preprocess-react'; const baseAlias = { @@ -63,7 +94,7 @@ function generateSveltePreprocessReactAliases() { } /** - * @type {(options:{ external?: { excludes:string[] } | boolean }) => import('vite').Plugin} + * @type {(options:{ external?: { excludes:string[] } | boolean }) => import('vite').Plugin[]} */ export const ModelScopeStudioVitePlugin = ({ external = true } = {}) => { const globals = external?.excludes @@ -74,129 +105,139 @@ export const ModelScopeStudioVitePlugin = ({ external = true } = {}) => { return aliases; }, {}) : baseGlobals; - return { - name: 'modelscope-studio-vite-plugin', - config(userConfig, { command }) { - const isBuild = command === 'build'; - if (isBuild) { - userConfig.define = { - ...userConfig.define, - 'process.env.NODE_ENV': JSON.stringify('production'), - }; - userConfig.build ??= {}; - if (external) { - userConfig.build.rollupOptions ??= {}; - userConfig.build.rollupOptions.external = [ - ...(userConfig.build.rollupOptions.external || []), - ...Object.keys(globals), - ]; + const externalNames = external ? Object.keys(globals) : []; + return [ + // Rolldown handles `require()` in bundled CJS modules through a + // runtime shim that throws in the browser. `esmExternalRequirePlugin` fixes + // that at the correct layer: it rewrites each such `require` into an ES + // import (via a virtual facade `import * as m from ''; module.exports + // = m`), so the shim never gets called and the resulting ES imports flow + // through our own renderChunk transform below, which turns them into + // `window.ms_globals.*` references alongside the code's native imports. + // + // The plugin owns the entire external list, so nothing gets pinned onto + // `build.rolldownOptions.external`; duplicating the list there would let + // the top-level `external` win during resolution and silently disable this + // plugin (rolldown docs). + ...(externalNames.length + ? [esmExternalRequirePlugin({ external: externalNames })] + : []), + { + name: 'modelscope-studio-vite-plugin', + config(userConfig, { command }) { + const isBuild = command === 'build'; + if (isBuild) { + userConfig.define = { + ...userConfig.define, + 'process.env.NODE_ENV': JSON.stringify('production'), + }; + userConfig.build ??= {}; } - } - userConfig.resolve ??= {}; - userConfig.resolve.alias = { - ...(userConfig.resolve.alias || {}), - '@utils': path.resolve(dirname, 'utils'), - '@globals': path.resolve(dirname, 'globals'), - ...generateSveltePreprocessReactAliases(), - }; - }, - renderChunk(code, chunk) { - const id = chunk.fileName; - if ( - ['.jsx', '.js', '.cjs', '.esm', '.tsx', '.ts'].some((ext) => - id.endsWith(ext) - ) - ) { - const ast = parse(code, { - sourceType: 'module', - }); - traverse(ast, { - Program: { - enter(enterPath) { - enterPath.traverse({ - ExportNamedDeclaration(nodePath) { - const source = nodePath.node.source?.value; - const entry = globals[source]; - if (!entry) { - return; - } - const { specifiers } = nodePath.node; + userConfig.resolve ??= {}; + userConfig.resolve.alias = { + ...(userConfig.resolve.alias || {}), + '@utils': path.resolve(dirname, 'utils'), + '@globals': path.resolve(dirname, 'globals'), + ...generateSveltePreprocessReactAliases(), + }; + }, + renderChunk(code, chunk) { + const id = chunk.fileName; + if ( + ['.jsx', '.js', '.cjs', '.esm', '.tsx', '.ts'].some((ext) => + id.endsWith(ext) + ) + ) { + const ast = parseSync(code, { + sourceType: 'module', + }); + traverse(ast, { + Program: { + enter(enterPath) { + enterPath.traverse({ + ExportNamedDeclaration(nodePath) { + const source = nodePath.node.source?.value; + const entry = globals[source]; + if (!entry) { + return; + } + const { specifiers } = nodePath.node; - const decls = specifiers.map((specifier) => { - return t.variableDeclarator( - specifier.local, - t.memberExpression( - t.identifier(entry.ref), - specifier.local - ) + const decls = specifiers.map((specifier) => { + return t.variableDeclarator( + specifier.local, + createGlobalMemberExpression(entry.ref, specifier.local) + ); + }); + nodePath.insertBefore( + t.variableDeclaration('const', decls) ); - }); - nodePath.insertBefore(t.variableDeclaration('const', decls)); - nodePath.insertAfter( - t.exportNamedDeclaration(null, specifiers) - ); - nodePath.remove(); - }, - ImportDeclaration(nodePath) { - const source = nodePath.node.source.value; - const entry = globals[source]; + nodePath.insertAfter( + t.exportNamedDeclaration(null, specifiers) + ); + nodePath.remove(); + }, + ImportDeclaration(nodePath) { + const source = nodePath.node.source.value; + const entry = globals[source]; - if (!entry) { - return; - } + if (!entry) { + return; + } - const { specifiers } = nodePath.node; - // eg: import "react"; - if (specifiers.length === 0) { - nodePath.remove(); - return; - } - const decls = specifiers.map((specifier) => { - switch (specifier.type) { - case 'ImportDefaultSpecifier': - // For namespace-style globals, the default export - // lives at `.default`. For value-style globals, - // `` itself already is the default value. - return t.variableDeclarator( - specifier.local, - entry.namespace - ? t.memberExpression( - t.identifier(entry.ref), - t.identifier('default') - ) - : t.identifier(entry.ref) - ); - case 'ImportSpecifier': - return t.variableDeclarator( - specifier.local, - t.memberExpression( - t.identifier(entry.ref), - specifier.imported - ) - ); - case 'ImportNamespaceSpecifier': - return t.variableDeclarator( - specifier.local, - t.identifier(entry.ref) - ); - default: - throw new Error( - `Unsupported import specifier type ${specifier.type}` - ); + const { specifiers } = nodePath.node; + // eg: import "react"; + if (specifiers.length === 0) { + nodePath.remove(); + return; } - }); - nodePath.insertAfter(t.variableDeclaration('const', decls)); - nodePath.remove(); - }, - }); + const decls = specifiers.map((specifier) => { + switch (specifier.type) { + case 'ImportDefaultSpecifier': + // For namespace-style globals, the default export + // lives at `.default`. For value-style globals, + // `` itself already is the default value. + return t.variableDeclarator( + specifier.local, + entry.namespace + ? createGlobalMemberExpression( + entry.ref, + t.identifier('default') + ) + : createGlobalExpression(entry.ref) + ); + case 'ImportSpecifier': + return t.variableDeclarator( + specifier.local, + createGlobalMemberExpression( + entry.ref, + specifier.imported + ) + ); + case 'ImportNamespaceSpecifier': + return t.variableDeclarator( + specifier.local, + createGlobalExpression(entry.ref) + ); + default: + throw new Error( + `Unsupported import specifier type ${specifier.type}` + ); + } + }); + nodePath.insertAfter(t.variableDeclaration('const', decls)); + nodePath.remove(); + }, + }); + }, }, - }, - }); - const transformed = transformFromAstSync(ast); + }); + const transformed = transformFromAstSync(ast); - return transformed.code; - } + return transformed.code; + } + }, }, - }; + ]; }; diff --git a/frontend/pro/chatbot/type.ts b/frontend/pro/chatbot/type.ts index e20a8801..fe3e3c7d 100644 --- a/frontend/pro/chatbot/type.ts +++ b/frontend/pro/chatbot/type.ts @@ -136,11 +136,7 @@ export interface ChatbotMessageContentObject { export interface ChatbotMessage extends ChatbotBotConfig { role: - | 'user' - | 'assistant' - | 'system' - | 'divider' - | 'chatbot-internal-welcome'; + 'user' | 'assistant' | 'system' | 'divider' | 'chatbot-internal-welcome'; divider_props?: DividerProps; key?: string | number; [lastMessageSymbol]?: boolean; diff --git a/frontend/pro/monaco-editor/loader.ts b/frontend/pro/monaco-editor/loader.ts index a66b4942..36658ae8 100644 --- a/frontend/pro/monaco-editor/loader.ts +++ b/frontend/pro/monaco-editor/loader.ts @@ -33,19 +33,19 @@ export async function initLocalLoader() { const [monaco, editorWorker, cssWorker, htmlWorker, jsonWorker, tsWorker] = await Promise.all([ import('monaco-editor'), - import('monaco-editor/esm/vs/editor/editor.worker?worker&inline').then( + import('monaco-editor/editor/editor.worker?worker&inline').then( (m) => m.default ), - import('monaco-editor/esm/vs/language/css/css.worker?worker&inline').then( + import('monaco-editor/language/css/css.worker?worker&inline').then( (m) => m.default ), - import('monaco-editor/esm/vs/language/html/html.worker?worker&inline').then( + import('monaco-editor/language/html/html.worker?worker&inline').then( (m) => m.default ), - import('monaco-editor/esm/vs/language/json/json.worker?worker&inline').then( + import('monaco-editor/language/json/json.worker?worker&inline').then( (m) => m.default ), - import('monaco-editor/esm/vs/language/typescript/ts.worker?worker&inline').then( + import('monaco-editor/language/typescript/ts.worker?worker&inline').then( (m) => m.default ), ]); diff --git a/frontend/pro/web-sandbox/web-sandbox.tsx b/frontend/pro/web-sandbox/web-sandbox.tsx index b72cb8b1..c164b2d9 100644 --- a/frontend/pro/web-sandbox/web-sandbox.tsx +++ b/frontend/pro/web-sandbox/web-sandbox.tsx @@ -262,8 +262,7 @@ export const WebSandbox = sveltify( const iframeMessageListener = (e: MessageEvent) => { if (e.source === iframeRef.current?.contentWindow) { const data = e.data as - | { type?: string; message?: string } - | undefined; + { type?: string; message?: string } | undefined; switch (data?.type) { case 'sandbox-error': if (showRenderError) { diff --git a/frontend/svelte-preprocess-react/component/props.svelte.ts b/frontend/svelte-preprocess-react/component/props.svelte.ts index bddb2631..45d872bf 100644 --- a/frontend/svelte-preprocess-react/component/props.svelte.ts +++ b/frontend/svelte-preprocess-react/component/props.svelte.ts @@ -270,11 +270,12 @@ export function processProps< } // for loading_status - if ( - shouldSetLoadingStatus && - props.additionalProps?.ms_auto_loading !== false - ) { - setLoadingStatus(() => props.restProps.loading_status); + if (shouldSetLoadingStatus) { + setLoadingStatus(() => + props.additionalProps?.ms_auto_loading === false + ? null + : props.restProps.loading_status + ); } // for ms.Each @@ -379,9 +380,7 @@ export function getProps< )), elem_id: gradio.shared.elem_id as string | undefined, elem_classes: gradio.shared.elem_classes as - | string - | string[] - | undefined, + string | string[] | undefined, elem_style: gradio.props.elem_style as React.CSSProperties, visible: gradio.shared.visible, attached_events: gradio.shared.attached_events, diff --git a/package.json b/package.json index ed23af15..a20ec155 100644 --- a/package.json +++ b/package.json @@ -6,18 +6,22 @@ "repository": "git@github.com:modelscope/modelscope-studio.git", "license": "Apache-2.0", "scripts": { + "prebuild": "pnpm run build:preview", "build": "rimraf dist && gradio cc build --no-generate-docs", + "build:preview": "pnpm -F '@modelscope-studio/gradio-preview' build", "changeset": "changeset", "check-encoding": "python ./scripts/check_encoding.py", "ci:create-tag-n-release": "tsx ./scripts/create-tag-n-release.mts", "ci:publish": "tsx ./scripts/publish-to-pypi.mts", "ci:version": "pnpm run version", + "predev": "pnpm run build:preview", "dev": "gradio cc dev docs/app.py", "fix-changelog": "node ./config/changelog/dist/fix-changelog.mjs", "format": "prettier --cache --write '**/*.{js,jsx,tsx,ts,svelte,less,md,yaml,yml,json}' --no-error-on-unmatched-pattern", + "gradio-preview:build": "pnpm -F '@modelscope-studio/gradio-preview' build", "lint": "npm-run-all --parallel lint:* && npm run format", "lint:js": "eslint . --fix --cache", - "lint:py": "pnpm run check-encoding && flake8 . && isort . && yapf --in-place --recursive --exclude=node_modules/ --exclude=docs/components/antdx/** .", + "lint:py": "pnpm run check-encoding && flake8 . && isort --extend-skip patches . && yapf --in-place --recursive --exclude=node_modules/ --exclude=docs/components/antdx/** .", "lint:style": "stylelint --allow-empty-input --cache --fix \"**/*.{less,css}\" ", "lint:ts": "tsc && svelte-check --tsconfig ./svelte-tsconfig.json", "prepare": "husky", @@ -27,28 +31,34 @@ "@actions/core": "^3.0.1", "@actions/exec": "^3.0.0", "@actions/github": "^9.1.1", - "@changesets/cli": "^2.31.0", - "@commitlint/cli": "^21.0.1", - "@commitlint/config-conventional": "^21.0.1", + "@changesets/cli": "^3.0.0", + "@commitlint/cli": "^21.2.1", + "@commitlint/config-conventional": "^21.2.0", "@manypkg/get-packages": "^3.1.0", "@modelscope-studio/changelog": "workspace:*", "@modelscope-studio/lint-config": "workspace:*", - "@types/node": "^25.8.0", - "eslint": "^10.4.0", + "@types/node": "^26.2.0", + "eslint": "^10.8.1", "husky": "^9.1.7", - "lint-staged": "^17.0.5", + "lint-staged": "^17.3.0", "node-fetch": "^3.3.2", "npm-run-all": "^4.1.5", - "prettier": "^3.8.3", + "prettier": "^3.9.6", "prettier-plugin-packagejson": "^3.0.2", - "prettier-plugin-svelte": "^3.5.2", + "prettier-plugin-svelte": "^4.1.1", "rimraf": "^6.1.3", - "stylelint": "^17.11.1", - "svelte": "5.55.7", - "svelte-check": "^4.4.8", + "stylelint": "^17.14.1", + "svelte": "5.48.0", + "svelte-check": "^4.7.5", "tslib": "^2.8.1", - "tsx": "^4.22.1", - "typescript": "^5.9.3" + "tsx": "^4.23.12", + "typescript": "^6.0.3" + }, + "pnpm": { + "overrides": { + "svelte": "5.48.0", + "tsup>typescript": "^5.9.3" + } }, "main_changeset": true } diff --git a/patches/gradio-preview/NOTICE b/patches/gradio-preview/NOTICE new file mode 100644 index 00000000..4aee2b86 --- /dev/null +++ b/patches/gradio-preview/NOTICE @@ -0,0 +1,20 @@ +@modelscope-studio/gradio-preview + +This package contains source code derived from Gradio +(https://github.com/gradio-app/gradio), specifically the `js/preview` package +(`@gradio/preview` 0.17.0, commit a9e8382c4). + +Copyright (c) Gradio contributors + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +Modifications made by the ModelScope team are described in README.md. diff --git a/patches/gradio-preview/README.md b/patches/gradio-preview/README.md new file mode 100644 index 00000000..511f8b75 --- /dev/null +++ b/patches/gradio-preview/README.md @@ -0,0 +1,91 @@ +# @modelscope-studio/gradio-preview + +Vendored copy of [`@gradio/preview`](https://github.com/gradio-app/gradio/tree/main/js/preview), +the Vite layer behind `gradio cc dev` and `gradio cc build`. + +It is vendored because the upstream package is tightly coupled to the Gradio app +bundle it drives, and the two have drifted: several dev-server problems can only +be fixed from inside this package (the `virtual:cc-init` module, the index-html +bootstrap, the build entry composition, the Vite version it ships with). + +## Why not just pin a stock `@gradio/preview`? + +- **`0.17.0` (Gradio 6.9+/6.23 era)** ships the right Vite (`vite ^8`, + `@sveltejs/vite-plugin-svelte ^7`) but has the dev-bootstrap bugs listed below. +- **`0.15.2` (Gradio <= 6.8 era)** boots the shared-runtime app correctly but + depends on `vite ^7` / `@sveltejs/vite-plugin-svelte ^6`, which is incompatible + with this repo's `vite 8` (rolldown) toolchain — it fails with + `Missing field 'moduleType'`. + +So this package takes the `0.17.0` source (for the `vite 8` deps + fixes) and +makes it work across both Gradio generations via `detect_generation`. + +## Vendor baseline + +| | | +| --- | --- | +| Upstream | `gradio-app/gradio` `js/preview` | +| Version | `0.17.0` | +| Commit | `a9e8382c4` | +| Deps | `vite ^8`, `@sveltejs/vite-plugin-svelte ^7` (kept from `0.17.0`) | + +Re-syncing means diffing `src/` against that path upstream, taking their changes +and re-applying the fixes below, then `pnpm -F @modelscope-studio/gradio-preview build`. + +## Local fixes + +1. **Dev bootstrap works on every Gradio generation.** The dev globals + (`__GRADIO_DEV__`, `__GRADIO__SERVER_PORT__`, `__GRADIO__CC__`, + `__GRADIO__CC__RUNTIMES__`) are injected into the page via + `transformIndexHtml`. Upstream only rewrites a `"_NORMAL_"` marker inside the + *built* app bundle, which silently misses when the minifier emits it as a + template literal (Gradio 6.23) and does not exist at all in Gradio <= 6.8 — + in both cases the app falls back to its own origin and asks the dev server + for `/config`, which answers with HTML. +2. **The Jinja template is neutralised.** The dev server hands `index.html` to + the browser unrendered, so `` (Gradio 6.9+) + would make every relative asset URL resolve against the raw template text + (blank page), and the inline `window.gradio_* = {{ ... }};` assignments are + syntax errors. +3. **Backend-owned routes are proxied** (`theme.css`, `manifest.json`, + `gradio_api`, `custom_component`). `theme.css` is resolved against the + document, so without this the app loads a stylesheet with zero rules and + renders with no Gradio CSS variables at all. +4. **Generation detection** (`detect_generation`), driving both dev and build: + - `shared` (Gradio <= 6.8) — the app exposes its runtime under + `assets/svelte/*`; components resolve `svelte` to it and share one instance. + `__GRADIO__CC__RUNTIMES__` is left empty and no per-component runtime entry + is built. + - `bridge` (Gradio >= ~6.9) — the runtime is inlined in the app bundle; + components bring their own and are mounted through `svelte_runtime_entry.js`. + + Mixing two runtime instances is what makes a snippet blow up with + `Cannot read properties of null (reading 'nodes')` as soon as rendering leaves + the synchronous `mount` call. +5. **`Pre-transform error` is no longer swallowed.** Upstream drops it from the + logger, which turns a failing module into an app that hangs with no output. + +## Consumer constraints (outside this package) + +`detect_generation` picks the right dev/build strategy, but it cannot fix +version-contract mismatches between the components and the Gradio app they run +against. When changing the target Gradio version, also align, in the repo root +and `frontend/`: + +- **`svelte`** must match the version Gradio bundles for the *shared* runtime + (`.../gradio/templates/frontend/assets/svelte/svelte_package.json.js`). + A newer compiler emits `$$restProps` excludes as a `Set` while an older + runtime expects an array, which throws + `target.exclude.includes is not a function`. Pin it repo-wide via + `pnpm.overrides.svelte` so a single version is resolved everywhere. +- **`@gradio/utils`** must match the app's registration contract: Gradio <= 6.8 + provides `register` via a Svelte context (`@gradio/utils` 0.11.x), Gradio 6.23 + provides `register_component` via `shared_props` (`@gradio/utils` 0.14.x). The + wrong one silently no-ops registration and the app hangs on its loading screen. + +Currently validated against **Gradio 6.8.0** (`shared`): `@gradio/utils@0.11.3`, +`@gradio/client@2.1.0`, `@gradio/statustracker@^0.13.1`, `svelte@5.48.0`. + +## License + +Derived from Gradio, licensed under Apache-2.0. See `NOTICE`. diff --git a/patches/gradio-preview/package.json b/patches/gradio-preview/package.json new file mode 100644 index 00000000..07001673 --- /dev/null +++ b/patches/gradio-preview/package.json @@ -0,0 +1,32 @@ +{ + "name": "@modelscope-studio/gradio-preview", + "version": "0.17.0", + "private": true, + "type": "module", + "description": "Vendored @gradio/preview with fixes for the custom component dev server", + "repository": "git@github.com:modelscope/modelscope-studio.git", + "license": "Apache-2.0", + "main": "dist/index.js", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "tsup && node ./scripts/copy-assets.mjs" + }, + "dependencies": { + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "rollup": "^4.59.0", + "svelte-preprocess": "^6.0.3", + "vite": "^8.0.9" + }, + "devDependencies": { + "@types/node": "^26.2.0", + "tsup": "^8.5.1", + "typescript": "^5.9.3" + } +} diff --git a/patches/gradio-preview/scripts/copy-assets.mjs b/patches/gradio-preview/scripts/copy-assets.mjs new file mode 100644 index 00000000..8cf5fa0e --- /dev/null +++ b/patches/gradio-preview/scripts/copy-assets.mjs @@ -0,0 +1,16 @@ +// `index.ts` spawns `examine.py` and `build.ts` uses `svelte_runtime_entry.js`, +// both resolved relative to the built entry, so they have to sit next to it. +import { copyFileSync, mkdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const assets = ['examine.py', 'svelte_runtime_entry.js']; + +mkdirSync(join(root, 'dist'), { recursive: true }); + +for (const asset of assets) { + copyFileSync(join(root, 'src', asset), join(root, 'dist', asset)); +} + +console.log(`copied ${assets.join(', ')} -> dist/`); diff --git a/patches/gradio-preview/src/_deepmerge_internal.ts b/patches/gradio-preview/src/_deepmerge_internal.ts new file mode 100644 index 00000000..a388c9bc --- /dev/null +++ b/patches/gradio-preview/src/_deepmerge_internal.ts @@ -0,0 +1,133 @@ +// this is a copy of the deepemerge source code but with an esm interface rather than commonjs + +export const deepmerge = ` +function isMergeableObject(value) { + return isNonNullObject(value) + && !isSpecial(value) +} + +function isNonNullObject(value) { + return !!value && typeof value === 'object' +} + +function isSpecial(value) { + var stringValue = Object.prototype.toString.call(value) + + return stringValue === '[object RegExp]' + || stringValue === '[object Date]' + || isReactElement(value) +} + +var canUseSymbol = typeof Symbol === 'function' && Symbol.for +var REACT_ELEMENT_TYPE = canUseSymbol ? Symbol.for('react.element') : 0xeac7 + +function isReactElement(value) { + return value.$$typeof === REACT_ELEMENT_TYPE +} + +var defaultIsMergeableObject = isMergeableObject; + +function emptyTarget(val) { + return Array.isArray(val) ? [] : {} +} + +function cloneUnlessOtherwiseSpecified(value, options) { + return (options.clone !== false && options.isMergeableObject(value)) + ? deepmerge(emptyTarget(value), value, options) + : value +} + +function defaultArrayMerge(target, source, options) { + return target.concat(source).map(function(element) { + return cloneUnlessOtherwiseSpecified(element, options) + }) +} + +function getMergeFunction(key, options) { + if (!options.customMerge) { + return deepmerge + } + var customMerge = options.customMerge(key) + return typeof customMerge === 'function' ? customMerge : deepmerge +} + +function getEnumerableOwnPropertySymbols(target) { + return Object.getOwnPropertySymbols + ? Object.getOwnPropertySymbols(target).filter(function(symbol) { + return Object.propertyIsEnumerable.call(target, symbol) + }) + : [] +} + +function getKeys(target) { + return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target)) +} + +function propertyIsOnObject(object, property) { + try { + return property in object + } catch(_) { + return false + } +} + +// Protects from prototype poisoning and unexpected merging up the prototype chain. +function propertyIsUnsafe(target, key) { + return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet, + && !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain, + && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable. +} + +function mergeObject(target, source, options) { + var destination = {} + if (options.isMergeableObject(target)) { + getKeys(target).forEach(function(key) { + destination[key] = cloneUnlessOtherwiseSpecified(target[key], options) + }) + } + getKeys(source).forEach(function(key) { + if (propertyIsUnsafe(target, key)) { + return + } + + if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) { + destination[key] = getMergeFunction(key, options)(target[key], source[key], options) + } else { + destination[key] = cloneUnlessOtherwiseSpecified(source[key], options) + } + }) + return destination +} + +function deepmerge(target, source, options) { + options = options || {} + options.arrayMerge = options.arrayMerge || defaultArrayMerge + options.isMergeableObject = options.isMergeableObject || defaultIsMergeableObject + + options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified + + var sourceIsArray = Array.isArray(source) + var targetIsArray = Array.isArray(target) + var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray + + if (!sourceAndTargetTypesMatch) { + return cloneUnlessOtherwiseSpecified(source, options) + } else if (sourceIsArray) { + return options.arrayMerge(target, source, options) + } else { + return mergeObject(target, source, options) + } +} + +deepmerge.all = function deepmergeAll(array, options) { + if (!Array.isArray(array)) { + throw new Error('first argument should be an array') + } + + return array.reduce(function(prev, next) { + return deepmerge(prev, next, options) + }, {}) +} + +export default deepmerge; +`; diff --git a/patches/gradio-preview/src/build.ts b/patches/gradio-preview/src/build.ts new file mode 100644 index 00000000..52b813fd --- /dev/null +++ b/patches/gradio-preview/src/build.ts @@ -0,0 +1,151 @@ +import * as fs from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath, pathToFileURL } from "url"; + +import { build } from "vite"; +import type { PreRenderedChunk } from "rollup"; + +import { plugins, make_gradio_plugin, detect_generation } from "./plugins"; +import { examine_module } from "./index"; + +interface BuildOptions { + component_dir: string; + root_dir: string; + python_path: string; +} + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export async function make_build({ + component_dir, + root_dir, + python_path +}: BuildOptions): Promise { + process.env.gradio_mode = "dev"; + const svelte_dir = join(root_dir, "assets", "svelte"); + + const module_meta = examine_module( + component_dir, + root_dir, + python_path, + "build" + ); + try { + for (const comp of module_meta) { + const template_dir = comp.template_dir; + const source_dir = comp.frontend_dir; + + const pkg = JSON.parse( + fs.readFileSync(join(source_dir, "package.json"), "utf-8") + ); + let component_config = { + plugins: [], + svelte: { + preprocess: [] + }, + build: { + target: [] + }, + optimizeDeps: { + exclude: ["svelte", "svelte/*"] + } + }; + + if ( + comp.frontend_dir && + fs.existsSync(join(comp.frontend_dir, "gradio.config.js")) + ) { + const m = await import( + pathToFileURL(join(comp.frontend_dir, "gradio.config.js")).href + ); + + component_config.plugins = m.default.plugins || []; + component_config.svelte.preprocess = m.default.svelte?.preprocess || []; + component_config.build.target = m.default.build?.target || "modules"; + component_config.optimizeDeps = + m.default.optimizeDeps || component_config.optimizeDeps; + } + + const generation = detect_generation(root_dir); + const exports: (string | any)[][] = [ + [ + join(template_dir, "component"), + [ + // The "shared" generation renders custom components with the app's + // own runtime, so there is nothing to hand a `mount` over — the + // extra entry would only ship a runtime the app never asks for. + ...(generation === "bridge" + ? [join(__dirname, "svelte_runtime_entry.js")] + : []), + join(source_dir, pkg.exports["."].gradio) + ] + ], + ...(pkg.exports["./example"] + ? [ + [ + join(template_dir, "example"), + [ + ...(generation === "bridge" + ? [join(__dirname, "svelte_runtime_entry.js")] + : []), + join(source_dir, pkg.exports["./example"].gradio) + ] + ] + ] + : []) + ]; + + for (const [out_path, entry_path] of exports) { + try { + const x = await build({ + root: source_dir, + configFile: false, + plugins: [ + ...plugins(component_config), + make_gradio_plugin({ + mode: "build", + svelte_dir, + component_dir, + root_dir + }) + ], + build: { + emptyOutDir: true, + outDir: out_path, + lib: { + entry: entry_path, + fileName: "index.js", + formats: ["es"] + }, + minify: true, + rollupOptions: { + output: { + assetFileNames: (chunkInfo) => { + if (chunkInfo.names[0].endsWith(".css")) { + return `style.css`; + } + + return chunkInfo.names[0]; + }, + entryFileNames: (chunkInfo: PreRenderedChunk) => { + if ( + chunkInfo.name.toLocaleLowerCase() === + "svelte_runtime_entry" + ) { + return "svelte_runtime_entry.js"; + } + return "index.js"; + } + } + } + } + }); + } catch (e) { + throw e; + } + } + } + } catch (e) { + throw e; + } +} diff --git a/patches/gradio-preview/src/dev.ts b/patches/gradio-preview/src/dev.ts new file mode 100644 index 00000000..d9c737ea --- /dev/null +++ b/patches/gradio-preview/src/dev.ts @@ -0,0 +1,247 @@ +import { join } from "path"; +import { pathToFileURL } from "url"; +import * as fs from "fs"; + +import { createServer, createLogger } from "vite"; +import type { PreprocessorGroup } from "svelte/compiler"; + +import { plugins, make_gradio_plugin, detect_generation } from "./plugins"; +import { examine_module } from "./index"; + +const vite_messages_to_ignore = [ + "Default and named imports from CSS files are deprecated.", + "The above dynamic import cannot be analyzed by Vite." +]; + +const logger = createLogger(); +const originalWarning = logger.warn; +logger.warn = (msg, options) => { + if (vite_messages_to_ignore.some((m) => msg.includes(m))) return; + + originalWarning(msg, options); +}; + +const originalError = logger.error; + +logger.error = (msg, options) => { + // Upstream swallows "Pre-transform error" here. Those are real failures — a + // module that fails to transform leaves the app hanging with no hint at all, + // so they are worth the noise. + originalError(msg, options); +}; + +interface ServerOptions { + component_dir: string; + root_dir: string; + frontend_port: number; + backend_port: number; + host: string; + python_path: string; +} + +export async function create_server({ + component_dir, + root_dir, + frontend_port, + backend_port, + host, + python_path +}: ServerOptions): Promise { + process.env.gradio_mode = "dev"; + const [imports, config, runtimes] = await generate_imports( + component_dir, + root_dir, + python_path + ); + + const svelte_dir = join(root_dir, "assets", "svelte"); + const backend_url = `http://localhost:${backend_port}`; + + try { + const server = await createServer({ + customLogger: logger, + mode: "development", + configFile: false, + root: root_dir, + server: { + port: frontend_port, + host: host, + // The app is served by the dev server, but whatever the backend owns + // has to stay reachable under the same origin: the app resolves + // `theme.css` against the document, so without this the dev server + // answers with its HTML fallback and the app renders unstyled. + proxy: { + "/theme.css": backend_url, + "/manifest.json": backend_url, + "/custom_component": backend_url, + "/gradio_api": { target: backend_url, ws: true } + }, + fs: { + allow: [root_dir, component_dir] + } + }, + optimizeDeps: config.optimizeDeps, + cacheDir: join(component_dir, "frontend", "node_modules", ".vite"), + plugins: [ + ...plugins(config), + make_gradio_plugin({ + mode: "dev", + backend_port, + svelte_dir, + component_dir, + root_dir, + imports, + runtimes + }) + ] + }); + + await server.listen(); + + console.info( + `[orange3]Frontend Server[/] (Go here): ${server.resolvedUrls?.local}` + ); + } catch (e) { + console.error(e); + } +} + +function find_frontend_folders(start_path: string): string[] { + if (!fs.existsSync(start_path)) { + console.warn("No directory found at:", start_path); + return []; + } + + if (fs.existsSync(join(start_path, "pyproject.toml"))) return [start_path]; + + const results: string[] = []; + const dir = fs.readdirSync(start_path); + dir.forEach((dir) => { + const filepath = join(start_path, dir); + if (fs.existsSync(filepath)) { + if (fs.existsSync(join(filepath, "pyproject.toml"))) + results.push(filepath); + } + }); + + return results; +} + +function to_posix(_path: string): string { + const isExtendedLengthPath = /^\\\\\?\\/.test(_path); + const hasNonAscii = /[^\u0000-\u0080]+/.test(_path); + + if (isExtendedLengthPath || hasNonAscii) { + return _path; + } + + return _path.replace(/\\/g, "/"); +} + +export interface ComponentConfig { + plugins: any[]; + svelte: { + preprocess: PreprocessorGroup[]; + extensions?: string[]; + }; + build: { + target: string | string[]; + }; + optimizeDeps: object; +} + +async function generate_imports( + component_dir: string, + root: string, + python_path: string +): Promise<[string, ComponentConfig, string]> { + const components = find_frontend_folders(component_dir); + + const component_entries = components.flatMap((component) => { + return examine_module(component, root, python_path, "dev"); + }); + if (component_entries.length === 0) { + console.info( + `No custom components were found in ${component_dir}. It is likely that dev mode does not work properly. Please pass the --gradio-path and --python-path CLI arguments so that gradio uses the right executables.` + ); + } + + let component_config: ComponentConfig = { + plugins: [], + svelte: { + preprocess: [] + }, + build: { + target: [] + }, + optimizeDeps: { + exclude: ["svelte", "svelte/*"] + } + }; + + await Promise.all( + component_entries.map(async (component) => { + if ( + component.frontend_dir && + fs.existsSync(join(component.frontend_dir, "gradio.config.js")) + ) { + const m = await import( + pathToFileURL(join(component.frontend_dir, "gradio.config.js")).href + ); + + component_config.plugins = m.default.plugins || []; + component_config.svelte.preprocess = m.default.svelte?.preprocess || []; + component_config.build.target = m.default.build?.target || "modules"; + component_config.optimizeDeps = + m.default.optimizeDeps || component_config.optimizeDeps; + } else { + } + }) + ); + + const imports = component_entries.reduce((acc, component) => { + const pkg = JSON.parse( + fs.readFileSync(join(component.frontend_dir, "package.json"), "utf-8") + ); + + const exports: Record = { + component: pkg.exports["."], + example: pkg.exports["./example"] + }; + + if (!exports.component) + throw new Error( + "Could not find component entry point. Please check the exports field of your package.json." + ); + + const example = exports.example + ? `example: () => import("/@fs/${to_posix( + join(component.frontend_dir, exports.example.gradio) + )}"),\n` + : ""; + return `${acc}"${component.component_class_id}": { + ${example} + component: () => import("/@fs/${to_posix( + join(component.frontend_dir, exports.component.gradio) + )}"), + + },\n`; + }, ""); + + const runtimes = + detect_generation(root) === "bridge" + ? component_entries.reduce((acc, component) => { + // The "bridge" app mounts each component with its own Svelte + // runtime instance. This eager `import("svelte")` is only ever read + // through the `virtual:cc-init` module, where Vite rewrites the + // bare specifier to a real URL. It must never be inlined into the + // page as-is. + return `${acc}"${component.component_class_id}": import("svelte"),\n`; + }, "") + : // The "shared" app renders components with its own runtime and never + // reads `__GRADIO__CC__RUNTIMES__`, so leave it empty — emitting the + // bare `import("svelte")` here would break the inlined bootstrap. + ""; + + return [`{${imports}}`, component_config, `{${runtimes}}`]; +} diff --git a/patches/gradio-preview/src/examine.py b/patches/gradio-preview/src/examine.py new file mode 100644 index 00000000..62327c59 --- /dev/null +++ b/patches/gradio-preview/src/examine.py @@ -0,0 +1,76 @@ +import argparse +import importlib +import inspect +import os +from pathlib import Path +import sys + +from tomlkit import dumps, parse + +from gradio.blocks import BlockContext +from gradio.components import Component + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Description of your program") + parser.add_argument("-m", "--mode", help="Build mode or dev mode") + args = parser.parse_args() + + try: + with open("../pyproject.toml") as f: + pyproject_source = f.read() + + pyproject_toml = parse(pyproject_source) + keywords = pyproject_toml["project"]["keywords"] + custom_component = ("gradio-custom-component" in keywords or + "gradio custom component" in keywords) + if not custom_component: + sys.exit(0) + + module_name = pyproject_toml["project"]["name"] + module = importlib.import_module(module_name) + + artifacts: list[str] = pyproject_toml["tool"]["hatch"]["build"]["artifacts"] + + def get_relative_path(path): + return ( + os.path.abspath(Path(__file__).parent / path) + .replace(os.path.abspath(os.getcwd()), "") + .lstrip("/") + ) + + for name in dir(module): + value = getattr(module, name) + if name.startswith("__"): + continue + + if inspect.isclass(value) and ( + issubclass(value, BlockContext) or issubclass(value, Component) + ): + file_location = Path(inspect.getfile(value)).parent + + found = [ + x + for x in artifacts + if get_relative_path(Path("..") / x) + == get_relative_path(file_location / value.TEMPLATE_DIR) + ] + if len(found) == 0: + artifact = "/" + (os.path.abspath(file_location / value.TEMPLATE_DIR) + .replace(os.path.abspath(Path("..")), "") + .lstrip("/")) + if artifact not in artifacts: + artifacts.append(artifact) + + print( + f"{name}~|~|~|~{os.path.abspath(file_location / value.TEMPLATE_DIR)}~|~|~|~{os.path.abspath(file_location / value.FRONTEND_DIR)}~|~|~|~{value.get_component_class_id()}" + ) + continue + + if args.mode == "build": + pyproject_toml["tool"]["hatch"]["build"]["artifacts"] = artifacts + + with open("../pyproject.toml", "w") as f: + f.write(dumps(pyproject_toml)) + except Exception as e: + _, _, exc_tb = sys.exc_info() + print(f"|EXCEPTION|:{str(e)}, examine.py line {exc_tb.tb_lineno}") \ No newline at end of file diff --git a/patches/gradio-preview/src/hooks.mjs b/patches/gradio-preview/src/hooks.mjs new file mode 100644 index 00000000..6edabab0 --- /dev/null +++ b/patches/gradio-preview/src/hooks.mjs @@ -0,0 +1,19 @@ +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export async function resolve(specifier, context, nextResolve) { + // Take an `import` or `require` specifier and resolve it to a URL. + if (specifier === "svelte/internal" || specifier === "svelte") { + return nextResolve( + join(__dirname, "frontend", "assets", "svelte", "svelte.js") + ); + } + if (specifier.startsWith("svelte/")) { + return nextResolve( + join(__dirname, "frontend", "assets", "svelte", "svelte-submodules.js") + ); + } + return nextResolve(specifier); +} diff --git a/patches/gradio-preview/src/index.ts b/patches/gradio-preview/src/index.ts new file mode 100644 index 00000000..e4cf51b3 --- /dev/null +++ b/patches/gradio-preview/src/index.ts @@ -0,0 +1,202 @@ +import { type ChildProcess, spawn, spawnSync } from "node:child_process"; +import * as net from "net"; + +import { create_server, type ComponentConfig } from "./dev"; +import { make_build } from "./build"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export interface ComponentMeta { + name: string; + template_dir: string; + frontend_dir: string; + component_class_id: string; +} + +const args = process.argv.slice(2); +// get individual args as `--arg value` or `value` + +function parse_args(args: string[]): Record { + const arg_map: Record = {}; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg.startsWith("--")) { + const name = arg.slice(2); + const value = args[i + 1]; + arg_map[name] = value; + i++; + } + } + return arg_map; +} + +const parsed_args = parse_args(args); + +async function run(): Promise { + if (parsed_args.mode === "build") { + await make_build({ + component_dir: parsed_args["component-directory"], + root_dir: parsed_args.root, + python_path: parsed_args["python-path"] + }); + } else { + const [backend_port, frontend_port] = await find_free_ports(7860, 8860); + const options = { + component_dir: parsed_args["component-directory"], + root_dir: parsed_args.root, + frontend_port, + backend_port, + host: parsed_args.host, + ...parsed_args + }; + process.env.GRADIO_BACKEND_PORT = backend_port.toString(); + const _process = spawn( + parsed_args["gradio-path"], + [parsed_args.app, "--watch-dirs", options.component_dir], + { + shell: false, + stdio: "pipe", + cwd: process.cwd(), + env: { + ...process.env, + GRADIO_SERVER_PORT: backend_port.toString(), + PYTHONUNBUFFERED: "true" + } + } + ); + + _process.stdout.setEncoding("utf8"); + _process.stderr.setEncoding("utf8"); + + function std_out(mode: "stdout" | "stderr") { + return function (data: Buffer): void { + const _data = data.toString(); + + if (_data.includes("Running on")) { + create_server({ + component_dir: options.component_dir, + root_dir: options.root_dir, + frontend_port, + backend_port, + host: options.host, + python_path: parsed_args["python-path"] + }); + } + + process[mode].write(_data); + }; + } + + _process.stdout.on("data", std_out("stdout")); + _process.stderr.on("data", std_out("stderr")); + _process.on("exit", () => kill_process(_process)); + _process.on("close", () => kill_process(_process)); + _process.on("disconnect", () => kill_process(_process)); + } +} + +function kill_process(process: ChildProcess): void { + process.kill("SIGKILL"); +} + +export { create_server }; + +run(); + +export async function find_free_ports( + start_port: number, + end_port: number +): Promise<[number, number]> { + let found_ports: number[] = []; + + for (let port = start_port; port < end_port; port++) { + if (await is_free_port(port)) { + found_ports.push(port); + if (found_ports.length === 2) { + return [found_ports[0], found_ports[1]]; + } + } + } + + throw new Error( + `Could not find free ports: there were not enough ports available.` + ); +} + +export function is_free_port(port: number): Promise { + return new Promise((accept, reject) => { + const sock = net.createConnection(port, "127.0.0.1"); + setTimeout(() => { + sock.destroy(); + reject( + new Error(`Timeout while detecting free port with 127.0.0.1:${port} `) + ); + }, 3000); + sock.once("connect", () => { + sock.end(); + accept(false); + }); + sock.once("error", (e) => { + sock.destroy(); + //@ts-ignore + if (e.code === "ECONNREFUSED") { + accept(true); + } else { + reject(e); + } + }); + }); +} + +function is_truthy(value: T | null | undefined | false): value is T { + return value !== null && value !== undefined && value !== false; +} + +export function examine_module( + component_dir: string, + root: string, + python_path: string, + mode: "build" | "dev" +): ComponentMeta[] { + const _process = spawnSync( + python_path, + [join(__dirname, "examine.py"), "-m", mode], + { + cwd: join(component_dir, "backend"), + stdio: "pipe" + } + ); + const exceptions: string[] = []; + + const components = _process.stdout + .toString() + .trim() + .split("\n") + .map((line) => { + if (line.startsWith("|EXCEPTION|")) { + exceptions.push(line.slice("|EXCEPTION|:".length)); + } + const [name, template_dir, frontend_dir, component_class_id] = + line.split("~|~|~|~"); + if (name && template_dir && frontend_dir && component_class_id) { + return { + name: name.trim(), + template_dir: template_dir.trim(), + frontend_dir: frontend_dir.trim(), + component_class_id: component_class_id.trim() + }; + } + return false; + }) + .filter(is_truthy); + if (exceptions.length > 0) { + console.info( + `While searching for gradio custom component source directories in ${component_dir}, the following exceptions were raised. If dev mode does not work properly please pass the --gradio-path and --python-path CLI arguments so that gradio uses the right executables: ${exceptions.join( + "\n" + )}` + ); + } + return components; +} diff --git a/patches/gradio-preview/src/placeholder.ts b/patches/gradio-preview/src/placeholder.ts new file mode 100644 index 00000000..ff8b4c56 --- /dev/null +++ b/patches/gradio-preview/src/placeholder.ts @@ -0,0 +1 @@ +export default {}; diff --git a/patches/gradio-preview/src/plugins.ts b/patches/gradio-preview/src/plugins.ts new file mode 100644 index 00000000..50dca9ec --- /dev/null +++ b/patches/gradio-preview/src/plugins.ts @@ -0,0 +1,266 @@ +import type { Plugin, PluginOption } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import { join, dirname } from "path"; +import { createRequire } from "module"; +import { existsSync, readFileSync } from "fs"; +import { type ComponentConfig } from "./dev"; +import type { PreprocessorGroup } from "svelte/compiler"; +import { sveltePreprocess } from "svelte-preprocess"; + +const svelte_codes_to_ignore: Record = { + "reactive-component": "Icon" +}; + +export function plugins(config: ComponentConfig): PluginOption[] { + const _additional_plugins = config.plugins || []; + const _additional_svelte_preprocess = config.svelte?.preprocess || []; + const _svelte_extensions = (config.svelte?.extensions || [".svelte"]).map( + (ext) => { + if (ext.trim().startsWith(".")) { + return ext; + } + return `.${ext.trim()}`; + } + ); + + if (!_svelte_extensions.includes(".svelte")) { + _svelte_extensions.push(".svelte"); + } + + return [ + svelte({ + inspector: false, + onwarn(warning, handler) { + if ( + svelte_codes_to_ignore.hasOwnProperty(warning.code) && + svelte_codes_to_ignore[warning.code] && + warning.message.includes(svelte_codes_to_ignore[warning.code]) + ) { + return; + } + handler!(warning); + }, + prebundleSvelteLibraries: false, + compilerOptions: { + discloseVersion: false, + hmr: true + }, + extensions: _svelte_extensions, + preprocess: [ + sveltePreprocess({ + typescript: { + compilerOptions: { + declaration: false, + declarationMap: false + } + } + }), + ...(_additional_svelte_preprocess as PreprocessorGroup[]) + ] + }), + ..._additional_plugins + ]; +} + +/** + * Gradio ships two generations of app bundle, and a custom component has to be + * wired differently for each: + * + * - "shared" (Gradio <= 6.8): the app exposes its own Svelte runtime as plain + * modules under `templates/frontend/assets/svelte/*`. Components import that + * runtime, so app and component share a single instance. The app has no + * `__MODE__` hook and never imports `virtual:cc-init`, so the dev globals + * have to be injected into the page directly. + * - "bridge" (Gradio >= ~6.9): the runtime is inlined into the app bundle. + * Components bring their own instance and are mounted through it, and the + * app boots into custom component mode via `globalThis.__MODE__`. + * + * Detecting this from the app build (rather than pinning a version) keeps one + * vendored copy working across upgrades. + */ +export type GradioGeneration = "shared" | "bridge"; + +export function detect_generation(root_dir: string): GradioGeneration { + return existsSync(join(root_dir, "assets", "svelte")) + ? "shared" + : "bridge"; +} + +function resolve_svelte_entry(id: string, base_dir: string): string | null { + const require_fn = createRequire(join(base_dir, "frontend", "_")); + try { + const svelte_pkg_path = require_fn.resolve("svelte/package.json"); + const svelte_dir = dirname(svelte_pkg_path); + const pkg = JSON.parse(readFileSync(svelte_pkg_path, "utf-8")); + + const subpath = id === "svelte" ? "." : "./" + id.slice("svelte/".length); + + if (pkg.exports && pkg.exports[subpath]) { + const entry = pkg.exports[subpath]; + const resolved = + typeof entry === "string" ? entry : entry.browser || entry.default; + if (resolved) { + return join(svelte_dir, resolved); + } + } + } catch { + return null; + } + return null; +} + +interface GradioPluginOptions { + mode: "dev" | "build"; + svelte_dir: string; + component_dir: string; + root_dir: string; + backend_port?: number; + imports?: string; + runtimes?: string; +} + +export function make_gradio_plugin({ + mode, + backend_port, + component_dir, + root_dir, + svelte_dir, + imports, + runtimes +}: GradioPluginOptions): Plugin { + const v_id = "virtual:component-loader"; + const v_id_2 = "virtual:cc-init"; + const resolved_v_id = "\0" + v_id; + const resolved_v_id_2 = "\0" + v_id_2; + const generation = detect_generation(root_dir); + const dev_globals = `window.__GRADIO_DEV__ = "dev"; + window.__GRADIO__SERVER_PORT__ = ${backend_port}; + window.__GRADIO__CC__ = ${imports}; + window.__GRADIO__CC__RUNTIMES__ = ${runtimes};`; + return { + name: "gradio", + enforce: "pre", + resolveId(id) { + if (id === v_id) { + return resolved_v_id; + } + if (id === v_id_2) { + return resolved_v_id_2; + } + + if (id === "svelte" || id.startsWith("svelte/")) { + // Only the `svelte` package and its subpaths share the app runtime. + // A prefix-only check would also swallow separate packages like + // `svelte-i18n`, whose slice happens to end up as `svelte_i18n.js`, + // a file the app does not serve. + // Point at the runtime the app itself uses, so that component and app + // share one instance. Mixing two instances breaks snippets and + // context: whichever runtime did not create an effect sees a `null` + // `active_effect` as soon as rendering leaves the synchronous mount. + if (generation === "shared") { + const subpath = + id === "svelte" ? "svelte" : id.slice("svelte/".length); + const file_name = `svelte_${subpath.replace(/\//g, "_")}.js`; + + // A built component is loaded from `