supervisely 6.73.438__py3-none-any.whl → 6.73.513__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. supervisely/__init__.py +137 -1
  2. supervisely/_utils.py +81 -0
  3. supervisely/annotation/annotation.py +8 -2
  4. supervisely/annotation/json_geometries_map.py +14 -11
  5. supervisely/annotation/label.py +80 -3
  6. supervisely/api/annotation_api.py +14 -11
  7. supervisely/api/api.py +59 -38
  8. supervisely/api/app_api.py +11 -2
  9. supervisely/api/dataset_api.py +74 -12
  10. supervisely/api/entities_collection_api.py +10 -0
  11. supervisely/api/entity_annotation/figure_api.py +52 -4
  12. supervisely/api/entity_annotation/object_api.py +3 -3
  13. supervisely/api/entity_annotation/tag_api.py +63 -12
  14. supervisely/api/guides_api.py +210 -0
  15. supervisely/api/image_api.py +72 -1
  16. supervisely/api/labeling_job_api.py +83 -1
  17. supervisely/api/labeling_queue_api.py +33 -7
  18. supervisely/api/module_api.py +9 -0
  19. supervisely/api/project_api.py +71 -26
  20. supervisely/api/storage_api.py +3 -1
  21. supervisely/api/task_api.py +13 -2
  22. supervisely/api/team_api.py +4 -3
  23. supervisely/api/video/video_annotation_api.py +119 -3
  24. supervisely/api/video/video_api.py +65 -14
  25. supervisely/api/video/video_figure_api.py +24 -11
  26. supervisely/app/__init__.py +1 -1
  27. supervisely/app/content.py +23 -7
  28. supervisely/app/development/development.py +18 -2
  29. supervisely/app/fastapi/__init__.py +1 -0
  30. supervisely/app/fastapi/custom_static_files.py +1 -1
  31. supervisely/app/fastapi/multi_user.py +105 -0
  32. supervisely/app/fastapi/subapp.py +88 -42
  33. supervisely/app/fastapi/websocket.py +77 -9
  34. supervisely/app/singleton.py +21 -0
  35. supervisely/app/v1/app_service.py +18 -2
  36. supervisely/app/v1/constants.py +7 -1
  37. supervisely/app/widgets/__init__.py +6 -0
  38. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  39. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  40. supervisely/app/widgets/activity_feed/style.css +78 -0
  41. supervisely/app/widgets/activity_feed/template.html +22 -0
  42. supervisely/app/widgets/card/card.py +20 -0
  43. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  44. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  45. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  46. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  47. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  48. supervisely/app/widgets/dialog/dialog.py +12 -0
  49. supervisely/app/widgets/dialog/template.html +2 -1
  50. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  51. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  52. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  53. supervisely/app/widgets/fast_table/template.html +1 -1
  54. supervisely/app/widgets/heatmap/__init__.py +0 -0
  55. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  56. supervisely/app/widgets/heatmap/script.js +533 -0
  57. supervisely/app/widgets/heatmap/style.css +233 -0
  58. supervisely/app/widgets/heatmap/template.html +21 -0
  59. supervisely/app/widgets/modal/__init__.py +0 -0
  60. supervisely/app/widgets/modal/modal.py +198 -0
  61. supervisely/app/widgets/modal/template.html +10 -0
  62. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  63. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  64. supervisely/app/widgets/radio_tabs/template.html +1 -0
  65. supervisely/app/widgets/select/select.py +6 -3
  66. supervisely/app/widgets/select_class/__init__.py +0 -0
  67. supervisely/app/widgets/select_class/select_class.py +363 -0
  68. supervisely/app/widgets/select_class/template.html +50 -0
  69. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  70. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  71. supervisely/app/widgets/select_tag/__init__.py +0 -0
  72. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  73. supervisely/app/widgets/select_tag/template.html +64 -0
  74. supervisely/app/widgets/select_team/select_team.py +37 -4
  75. supervisely/app/widgets/select_team/template.html +4 -5
  76. supervisely/app/widgets/select_user/__init__.py +0 -0
  77. supervisely/app/widgets/select_user/select_user.py +270 -0
  78. supervisely/app/widgets/select_user/template.html +13 -0
  79. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  80. supervisely/app/widgets/select_workspace/template.html +9 -12
  81. supervisely/app/widgets/table/table.py +68 -13
  82. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  83. supervisely/aug/aug.py +6 -2
  84. supervisely/convert/base_converter.py +1 -0
  85. supervisely/convert/converter.py +2 -2
  86. supervisely/convert/image/csv/csv_converter.py +24 -15
  87. supervisely/convert/image/image_converter.py +3 -1
  88. supervisely/convert/image/image_helper.py +48 -4
  89. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  90. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  91. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  92. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  93. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  94. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  95. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  96. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  97. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  98. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  99. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  100. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  101. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  102. supervisely/convert/video/__init__.py +1 -0
  103. supervisely/convert/video/multi_view/__init__.py +0 -0
  104. supervisely/convert/video/multi_view/multi_view.py +543 -0
  105. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  106. supervisely/convert/video/video_converter.py +24 -4
  107. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  108. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  109. supervisely/geometry/constants.py +1 -0
  110. supervisely/geometry/geometry.py +4 -0
  111. supervisely/geometry/helpers.py +5 -1
  112. supervisely/geometry/oriented_bbox.py +676 -0
  113. supervisely/geometry/polyline_3d.py +110 -0
  114. supervisely/geometry/rectangle.py +2 -1
  115. supervisely/io/env.py +76 -1
  116. supervisely/io/fs.py +21 -0
  117. supervisely/nn/benchmark/base_evaluator.py +104 -11
  118. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  119. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  120. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  121. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  122. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  123. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  124. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  125. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  126. supervisely/nn/inference/cache.py +43 -18
  127. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  128. supervisely/nn/inference/inference.py +916 -222
  129. supervisely/nn/inference/inference_request.py +55 -10
  130. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  131. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  132. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  133. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  134. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  135. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  136. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  137. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  138. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  139. supervisely/nn/inference/session.py +43 -35
  140. supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
  141. supervisely/nn/inference/tracking/point_tracking.py +5 -1
  142. supervisely/nn/inference/tracking/tracker_interface.py +10 -1
  143. supervisely/nn/inference/uploader.py +139 -12
  144. supervisely/nn/live_training/__init__.py +7 -0
  145. supervisely/nn/live_training/api_server.py +111 -0
  146. supervisely/nn/live_training/artifacts_utils.py +243 -0
  147. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  148. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  149. supervisely/nn/live_training/helpers.py +14 -0
  150. supervisely/nn/live_training/incremental_dataset.py +146 -0
  151. supervisely/nn/live_training/live_training.py +497 -0
  152. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  153. supervisely/nn/live_training/request_queue.py +52 -0
  154. supervisely/nn/model/model_api.py +9 -0
  155. supervisely/nn/model/prediction.py +2 -1
  156. supervisely/nn/model/prediction_session.py +26 -14
  157. supervisely/nn/prediction_dto.py +19 -1
  158. supervisely/nn/tracker/base_tracker.py +11 -1
  159. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  160. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  161. supervisely/nn/tracker/botsort_tracker.py +94 -65
  162. supervisely/nn/tracker/utils.py +4 -5
  163. supervisely/nn/tracker/visualize.py +93 -93
  164. supervisely/nn/training/gui/classes_selector.py +16 -1
  165. supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
  166. supervisely/nn/training/train_app.py +46 -31
  167. supervisely/project/data_version.py +115 -51
  168. supervisely/project/download.py +1 -1
  169. supervisely/project/pointcloud_episode_project.py +37 -8
  170. supervisely/project/pointcloud_project.py +30 -2
  171. supervisely/project/project.py +14 -2
  172. supervisely/project/project_meta.py +27 -1
  173. supervisely/project/project_settings.py +32 -18
  174. supervisely/project/versioning/__init__.py +1 -0
  175. supervisely/project/versioning/common.py +20 -0
  176. supervisely/project/versioning/schema_fields.py +35 -0
  177. supervisely/project/versioning/video_schema.py +221 -0
  178. supervisely/project/versioning/volume_schema.py +87 -0
  179. supervisely/project/video_project.py +717 -15
  180. supervisely/project/volume_project.py +623 -5
  181. supervisely/template/experiment/experiment.html.jinja +4 -4
  182. supervisely/template/experiment/experiment_generator.py +14 -21
  183. supervisely/template/live_training/__init__.py +0 -0
  184. supervisely/template/live_training/header.html.jinja +96 -0
  185. supervisely/template/live_training/live_training.html.jinja +51 -0
  186. supervisely/template/live_training/live_training_generator.py +464 -0
  187. supervisely/template/live_training/sly-style.css +402 -0
  188. supervisely/template/live_training/template.html.jinja +18 -0
  189. supervisely/versions.json +28 -26
  190. supervisely/video/sampling.py +39 -20
  191. supervisely/video/video.py +41 -12
  192. supervisely/video_annotation/video_figure.py +38 -4
  193. supervisely/video_annotation/video_object.py +29 -4
  194. supervisely/volume/stl_converter.py +2 -0
  195. supervisely/worker_api/agent_rpc.py +24 -1
  196. supervisely/worker_api/rpc_servicer.py +31 -7
  197. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
  198. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
  199. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  200. supervisely_lib/__init__.py +6 -1
  201. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  202. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  203. {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,352 @@
1
+ from typing import Callable, List, Optional, Union
2
+
3
+ from supervisely.annotation.tag_meta import TagMeta, TagValueType
4
+ from supervisely.annotation.tag_meta_collection import TagMetaCollection
5
+ from supervisely.app import DataJson, StateJson
6
+ from supervisely.app.widgets import Text, Widget
7
+ from supervisely.imaging.color import generate_rgb
8
+
9
+ try:
10
+ from typing import Literal
11
+ except ImportError:
12
+ from typing_extensions import Literal
13
+
14
+
15
+ # Available value types for tag creation
16
+ available_value_types = [
17
+ {"value": TagValueType.NONE, "label": "None"},
18
+ {"value": TagValueType.ANY_STRING, "label": "Any string"},
19
+ {"value": TagValueType.ANY_NUMBER, "label": "Any number"},
20
+ {"value": TagValueType.ONEOF_STRING, "label": "One of"},
21
+ ]
22
+
23
+
24
+ class SelectTag(Widget):
25
+ """
26
+ SelectTag is a compact dropdown widget for selecting tag metadata with an option to create
27
+ new tags on the fly.
28
+
29
+ :param tags: Initial list of TagMeta instances
30
+ :type tags: Optional[Union[List[TagMeta], TagMetaCollection]]
31
+ :param filterable: Enable search/filter functionality in dropdown
32
+ :type filterable: Optional[bool]
33
+ :param placeholder: Placeholder text when no tag is selected
34
+ :type placeholder: Optional[str]
35
+ :param show_add_new_tag: Show "Add new tag" option at the end of the list
36
+ :type show_add_new_tag: Optional[bool]
37
+ :param size: Size of the select dropdown
38
+ :type size: Optional[Literal["large", "small", "mini"]]
39
+ :param multiple: Enable multiple selection
40
+ :type multiple: bool
41
+ :param widget_id: Unique widget identifier
42
+ :type widget_id: Optional[str]
43
+
44
+ :Usage example:
45
+
46
+ .. code-block:: python
47
+
48
+ import supervisely as sly
49
+ from supervisely.app.widgets import SelectTag
50
+
51
+ # Create some initial tags
52
+ tag_weather = sly.TagMeta('weather', sly.TagValueType.ANY_STRING)
53
+ tag_count = sly.TagMeta('count', sly.TagValueType.ANY_NUMBER)
54
+
55
+ colors = ["red", "green", "blue"]
56
+ tag_color = sly.TagMeta('color', sly.TagValueType.ONEOF_STRING, possible_values=colors)
57
+
58
+ # Create SelectTag widget
59
+ select_tag = SelectTag(
60
+ tags=[tag_weather, tag_count, tag_color],
61
+ filterable=True,
62
+ show_add_new_tag=True
63
+ )
64
+
65
+ # Handle selection changes
66
+ @select_tag.value_changed
67
+ def on_tag_selected(tag_name):
68
+ print(f"Selected tag: {tag_name}")
69
+ selected_tag = select_tag.get_selected_tag()
70
+ print(f"Tag object: {selected_tag}")
71
+
72
+ # Handle new tag creation
73
+ @select_tag.tag_created
74
+ def on_tag_created(new_tag: sly.TagMeta):
75
+ print(f"New tag created: {new_tag.name}")
76
+ """
77
+
78
+ class Routes:
79
+ VALUE_CHANGED = "value_changed"
80
+ TAG_CREATED = "tag_created_cb"
81
+
82
+ def __init__(
83
+ self,
84
+ tags: Optional[Union[List[TagMeta], TagMetaCollection]] = [],
85
+ filterable: Optional[bool] = True,
86
+ placeholder: Optional[str] = "Select tag",
87
+ show_add_new_tag: Optional[bool] = True,
88
+ size: Optional[Literal["large", "small", "mini"]] = None,
89
+ multiple: bool = False,
90
+ widget_id: Optional[str] = None,
91
+ ):
92
+ # Convert to list for internal use to allow mutations when adding new tags
93
+ if isinstance(tags, TagMetaCollection):
94
+ self._tags = list(tags)
95
+ else:
96
+ self._tags = list(tags) if tags else []
97
+
98
+ self._filterable = filterable
99
+ self._placeholder = placeholder
100
+ self._show_add_new_tag = show_add_new_tag
101
+ self._size = size
102
+ self._multiple = multiple
103
+
104
+ self._changes_handled = False
105
+ self._tag_created_callback = None
106
+
107
+ # Store error message widget
108
+ self._error_message = Text("", status="error", font_size=13)
109
+
110
+ # Initialize parent Widget
111
+ super().__init__(widget_id=widget_id, file_path=__file__)
112
+
113
+ # Register tag_created route if show_add_new_tag is enabled
114
+ if self._show_add_new_tag:
115
+ self._register_tag_created_route()
116
+
117
+ def get_json_data(self):
118
+ """Build JSON data for the widget."""
119
+ # Build items list with tag info
120
+ items = []
121
+ for tag in self._tags:
122
+ value_type_text = tag.value_type.replace("_", " ").upper()
123
+
124
+ items.append(
125
+ {
126
+ "value": tag.name,
127
+ "label": tag.name,
128
+ "color": tag.color,
129
+ "valueType": value_type_text if value_type_text else "",
130
+ }
131
+ )
132
+
133
+ return {
134
+ "items": items,
135
+ "placeholder": self._placeholder,
136
+ "filterable": self._filterable,
137
+ "multiple": self._multiple,
138
+ "size": self._size,
139
+ "showAddNewTag": self._show_add_new_tag,
140
+ "availableValueTypes": available_value_types,
141
+ }
142
+
143
+ def get_json_state(self):
144
+ """Build JSON state for the widget."""
145
+ # Set initial value based on multiple mode
146
+ if self._multiple:
147
+ initial_value = []
148
+ else:
149
+ initial_value = self._tags[0].name if self._tags else None
150
+
151
+ return {
152
+ "value": initial_value,
153
+ "createTagDialog": {
154
+ "visible": False,
155
+ "tagName": "",
156
+ "valueType": TagValueType.NONE,
157
+ "possibleValues": "",
158
+ "showError": False,
159
+ "showPossibleValues": False,
160
+ },
161
+ }
162
+
163
+ def get_value(self) -> Union[str, List[str], None]:
164
+ """Get the currently selected tag name(s)."""
165
+ return StateJson()[self.widget_id]["value"]
166
+
167
+ def get_selected_tag(self) -> Union[TagMeta, List[TagMeta], None]:
168
+ """Get the currently selected TagMeta object(s)."""
169
+ value = self.get_value()
170
+ if value is None:
171
+ return None
172
+
173
+ if self._multiple:
174
+ if not isinstance(value, list):
175
+ return []
176
+ result = []
177
+ for tag_name in value:
178
+ for tag in self._tags:
179
+ if tag.name == tag_name:
180
+ result.append(tag)
181
+ break
182
+ return result
183
+ else:
184
+ for tag in self._tags:
185
+ if tag.name == value:
186
+ return tag
187
+ return None
188
+
189
+ def set_value(self, tag_name: Union[str, List[str]]):
190
+ """Set the selected tag by name."""
191
+ StateJson()[self.widget_id]["value"] = tag_name
192
+ StateJson().send_changes()
193
+
194
+ def get_all_tags(self) -> List[TagMeta]:
195
+ """Get all available tags."""
196
+ return self._tags.copy()
197
+
198
+ def set(self, tags: Union[List[TagMeta], TagMetaCollection]):
199
+ """Update the list of available tags."""
200
+ # Convert to list for internal use
201
+ if isinstance(tags, TagMetaCollection):
202
+ self._tags = list(tags)
203
+ else:
204
+ self._tags = list(tags) if tags else []
205
+
206
+ # Update data
207
+ DataJson()[self.widget_id] = self.get_json_data()
208
+ DataJson().send_changes()
209
+
210
+ # Reset value if current selection is not in new tags
211
+ current_value = StateJson()[self.widget_id]["value"]
212
+ if current_value:
213
+ if self._multiple:
214
+ if isinstance(current_value, list):
215
+ # Keep only valid selections
216
+ valid = [v for v in current_value if any(tag.name == v for tag in self._tags)]
217
+ if valid != current_value:
218
+ StateJson()[self.widget_id]["value"] = valid
219
+ StateJson().send_changes()
220
+ else:
221
+ if not any(tag.name == current_value for tag in self._tags):
222
+ StateJson()[self.widget_id]["value"] = (
223
+ self._tags[0].name if self._tags else None
224
+ )
225
+ StateJson().send_changes()
226
+
227
+ def _show_error(self, message: str):
228
+ """Show error message in the create tag dialog."""
229
+ self._error_message.text = message
230
+ StateJson()[self.widget_id]["createTagDialog"]["showError"] = True
231
+ StateJson().send_changes()
232
+
233
+ def _hide_dialog(self):
234
+ """Hide the create tag dialog and reset its state."""
235
+ state_obj = StateJson()[self.widget_id]["createTagDialog"]
236
+ state_obj["visible"] = False
237
+ state_obj["tagName"] = ""
238
+ state_obj["possibleValues"] = ""
239
+ state_obj["showError"] = False
240
+ state_obj["showPossibleValues"] = False
241
+ StateJson().send_changes()
242
+
243
+ def _add_new_tag(self, new_tag: TagMeta):
244
+ """Add a new tag to the widget and update the UI."""
245
+ # Add to tags list
246
+ self._tags.append(new_tag)
247
+
248
+ # Update data
249
+ DataJson()[self.widget_id] = self.get_json_data()
250
+ DataJson().send_changes()
251
+
252
+ # Set the new tag as selected
253
+ if self._multiple:
254
+ current = StateJson()[self.widget_id]["value"]
255
+ if isinstance(current, list):
256
+ current.append(new_tag.name)
257
+ else:
258
+ StateJson()[self.widget_id]["value"] = [new_tag.name]
259
+ else:
260
+ StateJson()[self.widget_id]["value"] = new_tag.name
261
+ StateJson().send_changes()
262
+
263
+ def value_changed(self, func: Callable[[Union[TagMeta, List[TagMeta]]], None]):
264
+ """
265
+ Decorator to handle value change event.
266
+ The decorated function receives the selected TagMeta (or list of TagMeta if multiple=True).
267
+
268
+ :param func: Function to be called when selection changes
269
+ :type func: Callable[[Union[TagMeta, List[TagMeta]]], None]
270
+ """
271
+ route_path = self.get_route_path(SelectTag.Routes.VALUE_CHANGED)
272
+ server = self._sly_app.get_server()
273
+ self._changes_handled = True
274
+
275
+ @server.post(route_path)
276
+ def _value_changed():
277
+ selected = self.get_selected_tag()
278
+ if selected is not None:
279
+ func(selected)
280
+
281
+ return _value_changed
282
+
283
+ def _register_tag_created_route(self):
284
+ """Register the tag_created route."""
285
+ route_path = self.get_route_path(SelectTag.Routes.TAG_CREATED)
286
+ server = self._sly_app.get_server()
287
+
288
+ @server.post(route_path)
289
+ def _tag_created():
290
+ state = StateJson()[self.widget_id]["createTagDialog"]
291
+ tag_name = state["tagName"].strip()
292
+ value_type = state["valueType"]
293
+ possible_values_str = state["possibleValues"].strip()
294
+
295
+ # Validate tag name
296
+ if not tag_name:
297
+ self._show_error("Tag name cannot be empty")
298
+ return
299
+
300
+ # Check if tag with this name already exists
301
+ if any(tag.name == tag_name for tag in self._tags):
302
+ self._show_error(f"Tag '{tag_name}' already exists")
303
+ return
304
+
305
+ # Parse possible values for ONEOF_STRING
306
+ possible_values = None
307
+ if value_type == TagValueType.ONEOF_STRING:
308
+ if not possible_values_str:
309
+ self._show_error("Possible values are required for 'One of' type")
310
+ return
311
+ # Split by comma and strip whitespace
312
+ possible_values = [v.strip() for v in possible_values_str.split(",") if v.strip()]
313
+ if len(possible_values) == 0:
314
+ self._show_error("At least one possible value is required")
315
+ return
316
+
317
+ # Generate color for the new tag
318
+ existing_colors = [tag.color for tag in self._tags]
319
+ new_color = generate_rgb(existing_colors)
320
+
321
+ # Create new tag
322
+ try:
323
+ new_tag = TagMeta(
324
+ name=tag_name,
325
+ value_type=value_type,
326
+ possible_values=possible_values,
327
+ color=new_color,
328
+ )
329
+ except Exception as e:
330
+ self._show_error(f"Error creating tag: {str(e)}")
331
+ return
332
+
333
+ # Add to widget
334
+ self._add_new_tag(new_tag)
335
+
336
+ # Hide dialog
337
+ self._hide_dialog()
338
+
339
+ # Call user's callback if set
340
+ if self._tag_created_callback:
341
+ self._tag_created_callback(new_tag)
342
+
343
+ def tag_created(self, func: Callable[[TagMeta], None]):
344
+ """
345
+ Decorator to handle new tag creation event.
346
+ The decorated function receives the newly created TagMeta.
347
+
348
+ :param func: Function to be called when a new tag is created
349
+ :type func: Callable[[TagMeta], None]
350
+ """
351
+ self._tag_created_callback = func
352
+ return func
@@ -0,0 +1,64 @@
1
+ <el-select v-model="state.{{{widget.widget_id}}}.value" {% if widget._changes_handled==true %}
2
+ @change="post('/{{{widget.widget_id}}}/value_changed')" {% endif %}
3
+ :placeholder="data.{{{widget.widget_id}}}.placeholder" :filterable="data.{{{widget.widget_id}}}.filterable"
4
+ :multiple="data.{{{widget.widget_id}}}.multiple" :size="data.{{{widget.widget_id}}}.size">
5
+ <el-option v-for="item in data.{{{widget.widget_id}}}.items" :key="item.value" :label="item.label"
6
+ :value="item.value">
7
+ <i class="zmdi zmdi-circle"
8
+ :style="{color: 'rgb(' + item.color[0] + ',' + item.color[1] + ',' + item.color[2] + ')'}"></i>
9
+ {{ item.label }}
10
+ <span v-if="item.valueType" style="float: right; color: #8492a6; font-size: 12px; margin-right: 20px;">
11
+ {{ item.valueType }}
12
+ </span>
13
+ </el-option>
14
+ <el-option v-if="data.{{{widget.widget_id}}}.showAddNewTag" value="__add_new_tag__" label="+ Add new tag" disabled
15
+ @click.native.stop="state.{{{widget.widget_id}}}.createTagDialog.visible = true" style="cursor: pointer;">
16
+ <span style="color: #409EFF; font-weight: 500;">+ Add new tag</span>
17
+ </el-option>
18
+ </el-select>
19
+
20
+
21
+ <!-- Dialog for creating new tag -->
22
+ <el-dialog title="Create New Tag" :visible.sync="state.{{{widget.widget_id}}}.createTagDialog.visible" width="500px"
23
+ :close-on-click-modal="false" @open="state.{{{widget.widget_id}}}.createTagDialog.showError = false">
24
+ <el-form label-width="140px" label-position="left">
25
+ <el-form-item label="Tag Name">
26
+ <el-input v-model="state.{{{widget.widget_id}}}.createTagDialog.tagName" placeholder="Enter tag name"
27
+ @input="state.{{{widget.widget_id}}}.createTagDialog.showError = false"
28
+ @keyup.enter.native="post('/{{{widget.widget_id}}}/tag_created_cb')"></el-input>
29
+ </el-form-item>
30
+ <el-form-item label="Value Type">
31
+ <el-select v-model="state.{{{widget.widget_id}}}.createTagDialog.valueType"
32
+ @change="state.{{{widget.widget_id}}}.createTagDialog.showPossibleValues = (state.{{{widget.widget_id}}}.createTagDialog.valueType === 'oneof_string')"
33
+ placeholder="Select value type" style="width: 100%">
34
+ <el-option v-for="vtype in data.{{{widget.widget_id}}}.availableValueTypes" :key="vtype.value"
35
+ :label="vtype.label" :value="vtype.value">
36
+ </el-option>
37
+ </el-select>
38
+ </el-form-item>
39
+ <el-form-item v-if="state.{{{widget.widget_id}}}.createTagDialog.showPossibleValues" label="Possible Values">
40
+ <el-input v-model="state.{{{widget.widget_id}}}.createTagDialog.possibleValues" type="textarea" :rows="3"
41
+ placeholder="red, green, blue"></el-input>
42
+ <div style="color: #909399; font-size: 11px; margin-top: 5px;">
43
+ Comma-separated values
44
+ </div>
45
+ </el-form-item>
46
+ <div v-show="state.{{{widget.widget_id}}}.createTagDialog.showError" style="margin-top: 10px;">
47
+ {{{widget._error_message}}}
48
+ </div>
49
+ </el-form>
50
+ <span slot="footer" class="dialog-footer">
51
+ <el-button @click="state.{{{widget.widget_id}}}.createTagDialog.visible = false">
52
+ Cancel
53
+ </el-button>
54
+ <el-button type="primary" @click="post('/{{{widget.widget_id}}}/tag_created_cb')">
55
+ Create
56
+ </el-button>
57
+ </span>
58
+ </el-dialog>
59
+
60
+ <style scoped>
61
+ .el-select-dropdown__item.is-disabled {
62
+ cursor: pointer !important;
63
+ }
64
+ </style>
@@ -1,17 +1,20 @@
1
- from typing import Dict
1
+ from typing import Callable, Dict
2
2
 
3
3
  try:
4
4
  from typing import Literal
5
5
  except ImportError:
6
6
  from typing_extensions import Literal
7
7
 
8
+ from supervisely.api.api import Api
8
9
  from supervisely.app import StateJson
9
10
  from supervisely.app.widgets import Widget
10
- from supervisely.api.api import Api
11
11
  from supervisely.app.widgets.select_sly_utils import _get_int_or_env
12
12
 
13
13
 
14
14
  class SelectTeam(Widget):
15
+ class Routes:
16
+ VALUE_CHANGED = "value_changed"
17
+
15
18
  def __init__(
16
19
  self,
17
20
  default_id: int = None,
@@ -23,10 +26,11 @@ class SelectTeam(Widget):
23
26
  self._default_id = default_id
24
27
  self._show_label = show_label
25
28
  self._size = size
29
+ self._changes_handled = False
26
30
 
27
31
  self._default_id = _get_int_or_env(self._default_id, "context.teamId")
28
32
  if self._default_id is not None:
29
- info = self._api.team.get_info_by_id(self._default_id, raise_error=True)
33
+ self._api.team.get_info_by_id(self._default_id, raise_error=True)
30
34
  super().__init__(widget_id=widget_id, file_path=__file__)
31
35
 
32
36
  def get_json_data(self) -> Dict:
@@ -48,4 +52,33 @@ class SelectTeam(Widget):
48
52
  }
49
53
 
50
54
  def get_selected_id(self):
51
- return StateJson()[self.widget_id]["teamId"]
55
+ return StateJson()[self.widget_id].get("teamId")
56
+
57
+ def set_team_id(self, team_id: int):
58
+ """Set the selected team ID.
59
+
60
+ :param team_id: Team ID to select
61
+ :type team_id: int
62
+ """
63
+ StateJson()[self.widget_id]["teamId"] = team_id
64
+ StateJson().send_changes()
65
+
66
+ def value_changed(self, func: Callable[[int], None]):
67
+ """
68
+ Decorator to handle team selection change event.
69
+ The decorated function receives the selected team ID.
70
+
71
+ :param func: Function to be called when team selection changes
72
+ :type func: Callable[[int], None]
73
+ """
74
+ route_path = self.get_route_path(SelectTeam.Routes.VALUE_CHANGED)
75
+ server = self._sly_app.get_server()
76
+ self._changes_handled = True
77
+
78
+ @server.post(route_path)
79
+ def _value_changed():
80
+ team_id = self.get_selected_id()
81
+ if team_id is not None:
82
+ func(team_id)
83
+
84
+ return _value_changed
@@ -1,5 +1,4 @@
1
- <sly-select-team-workspace
2
- :team-id.sync="state.{{{widget.widget_id}}}.teamId"
3
- :options="data.{{{widget.widget_id}}}.options"
4
- :disabled="data.{{{widget.widget_id}}}.disabled"
5
- ></sly-select-team-workspace>
1
+ <sly-select-team-workspace :team-id.sync="state.{{{widget.widget_id}}}.teamId" {% if widget._changes_handled==true %}
2
+ @update:team-id="state.{{{widget.widget_id}}}.teamId = $event; post('/{{{widget.widget_id}}}/value_changed')" {% endif
3
+ %} :options="data.{{{widget.widget_id}}}.options"
4
+ :disabled="data.{{{widget.widget_id}}}.disabled"></sly-select-team-workspace>
File without changes