supervisely 6.73.452__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 (189) hide show
  1. supervisely/__init__.py +25 -1
  2. supervisely/annotation/annotation.py +8 -2
  3. supervisely/annotation/json_geometries_map.py +13 -12
  4. supervisely/api/annotation_api.py +6 -3
  5. supervisely/api/api.py +2 -0
  6. supervisely/api/app_api.py +10 -1
  7. supervisely/api/dataset_api.py +74 -12
  8. supervisely/api/entities_collection_api.py +10 -0
  9. supervisely/api/entity_annotation/figure_api.py +28 -0
  10. supervisely/api/entity_annotation/object_api.py +3 -3
  11. supervisely/api/entity_annotation/tag_api.py +63 -12
  12. supervisely/api/guides_api.py +210 -0
  13. supervisely/api/image_api.py +4 -0
  14. supervisely/api/labeling_job_api.py +83 -1
  15. supervisely/api/labeling_queue_api.py +33 -7
  16. supervisely/api/module_api.py +5 -0
  17. supervisely/api/project_api.py +71 -26
  18. supervisely/api/storage_api.py +3 -1
  19. supervisely/api/task_api.py +13 -2
  20. supervisely/api/team_api.py +4 -3
  21. supervisely/api/video/video_annotation_api.py +119 -3
  22. supervisely/api/video/video_api.py +65 -14
  23. supervisely/app/__init__.py +1 -1
  24. supervisely/app/content.py +23 -7
  25. supervisely/app/development/development.py +18 -2
  26. supervisely/app/fastapi/__init__.py +1 -0
  27. supervisely/app/fastapi/custom_static_files.py +1 -1
  28. supervisely/app/fastapi/multi_user.py +105 -0
  29. supervisely/app/fastapi/subapp.py +88 -42
  30. supervisely/app/fastapi/websocket.py +77 -9
  31. supervisely/app/singleton.py +21 -0
  32. supervisely/app/v1/app_service.py +18 -2
  33. supervisely/app/v1/constants.py +7 -1
  34. supervisely/app/widgets/__init__.py +6 -0
  35. supervisely/app/widgets/activity_feed/__init__.py +0 -0
  36. supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
  37. supervisely/app/widgets/activity_feed/style.css +78 -0
  38. supervisely/app/widgets/activity_feed/template.html +22 -0
  39. supervisely/app/widgets/card/card.py +20 -0
  40. supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
  41. supervisely/app/widgets/classes_list_selector/template.html +60 -93
  42. supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
  43. supervisely/app/widgets/classes_table/classes_table.py +1 -0
  44. supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
  45. supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
  46. supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
  47. supervisely/app/widgets/fast_table/fast_table.py +184 -60
  48. supervisely/app/widgets/fast_table/template.html +1 -1
  49. supervisely/app/widgets/heatmap/__init__.py +0 -0
  50. supervisely/app/widgets/heatmap/heatmap.py +564 -0
  51. supervisely/app/widgets/heatmap/script.js +533 -0
  52. supervisely/app/widgets/heatmap/style.css +233 -0
  53. supervisely/app/widgets/heatmap/template.html +21 -0
  54. supervisely/app/widgets/modal/__init__.py +0 -0
  55. supervisely/app/widgets/modal/modal.py +198 -0
  56. supervisely/app/widgets/modal/template.html +10 -0
  57. supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
  58. supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
  59. supervisely/app/widgets/radio_tabs/template.html +1 -0
  60. supervisely/app/widgets/select/select.py +6 -3
  61. supervisely/app/widgets/select_class/__init__.py +0 -0
  62. supervisely/app/widgets/select_class/select_class.py +363 -0
  63. supervisely/app/widgets/select_class/template.html +50 -0
  64. supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
  65. supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
  66. supervisely/app/widgets/select_tag/__init__.py +0 -0
  67. supervisely/app/widgets/select_tag/select_tag.py +352 -0
  68. supervisely/app/widgets/select_tag/template.html +64 -0
  69. supervisely/app/widgets/select_team/select_team.py +37 -4
  70. supervisely/app/widgets/select_team/template.html +4 -5
  71. supervisely/app/widgets/select_user/__init__.py +0 -0
  72. supervisely/app/widgets/select_user/select_user.py +270 -0
  73. supervisely/app/widgets/select_user/template.html +13 -0
  74. supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
  75. supervisely/app/widgets/select_workspace/template.html +9 -12
  76. supervisely/app/widgets/table/table.py +68 -13
  77. supervisely/app/widgets/tree_select/tree_select.py +2 -0
  78. supervisely/aug/aug.py +6 -2
  79. supervisely/convert/base_converter.py +1 -0
  80. supervisely/convert/converter.py +2 -2
  81. supervisely/convert/image/image_converter.py +3 -1
  82. supervisely/convert/image/image_helper.py +48 -4
  83. supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
  84. supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
  85. supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
  86. supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
  87. supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
  88. supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
  89. supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
  90. supervisely/convert/pointcloud/las/las_converter.py +13 -1
  91. supervisely/convert/pointcloud/las/las_helper.py +110 -11
  92. supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
  93. supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
  94. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
  95. supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
  96. supervisely/convert/video/__init__.py +1 -0
  97. supervisely/convert/video/multi_view/__init__.py +0 -0
  98. supervisely/convert/video/multi_view/multi_view.py +543 -0
  99. supervisely/convert/video/sly/sly_video_converter.py +359 -3
  100. supervisely/convert/video/video_converter.py +22 -2
  101. supervisely/convert/volume/dicom/dicom_converter.py +13 -5
  102. supervisely/convert/volume/dicom/dicom_helper.py +30 -18
  103. supervisely/geometry/constants.py +1 -0
  104. supervisely/geometry/geometry.py +4 -0
  105. supervisely/geometry/helpers.py +5 -1
  106. supervisely/geometry/oriented_bbox.py +676 -0
  107. supervisely/geometry/rectangle.py +2 -1
  108. supervisely/io/env.py +76 -1
  109. supervisely/io/fs.py +21 -0
  110. supervisely/nn/benchmark/base_evaluator.py +104 -11
  111. supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
  112. supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
  113. supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
  114. supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
  115. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
  116. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
  117. supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
  118. supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
  119. supervisely/nn/inference/cache.py +43 -18
  120. supervisely/nn/inference/gui/serving_gui_template.py +5 -2
  121. supervisely/nn/inference/inference.py +795 -199
  122. supervisely/nn/inference/inference_request.py +42 -9
  123. supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
  124. supervisely/nn/inference/predict_app/gui/gui.py +676 -488
  125. supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
  126. supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
  127. supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
  128. supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
  129. supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
  130. supervisely/nn/inference/predict_app/gui/utils.py +236 -119
  131. supervisely/nn/inference/predict_app/predict_app.py +2 -2
  132. supervisely/nn/inference/session.py +43 -35
  133. supervisely/nn/inference/tracking/bbox_tracking.py +113 -34
  134. supervisely/nn/inference/tracking/tracker_interface.py +7 -2
  135. supervisely/nn/inference/uploader.py +139 -12
  136. supervisely/nn/live_training/__init__.py +7 -0
  137. supervisely/nn/live_training/api_server.py +111 -0
  138. supervisely/nn/live_training/artifacts_utils.py +243 -0
  139. supervisely/nn/live_training/checkpoint_utils.py +229 -0
  140. supervisely/nn/live_training/dynamic_sampler.py +44 -0
  141. supervisely/nn/live_training/helpers.py +14 -0
  142. supervisely/nn/live_training/incremental_dataset.py +146 -0
  143. supervisely/nn/live_training/live_training.py +497 -0
  144. supervisely/nn/live_training/loss_plateau_detector.py +111 -0
  145. supervisely/nn/live_training/request_queue.py +52 -0
  146. supervisely/nn/model/model_api.py +9 -0
  147. supervisely/nn/prediction_dto.py +12 -1
  148. supervisely/nn/tracker/base_tracker.py +11 -1
  149. supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
  150. supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
  151. supervisely/nn/tracker/botsort_tracker.py +94 -65
  152. supervisely/nn/tracker/visualize.py +87 -90
  153. supervisely/nn/training/gui/classes_selector.py +16 -1
  154. supervisely/nn/training/train_app.py +28 -29
  155. supervisely/project/data_version.py +115 -51
  156. supervisely/project/download.py +1 -1
  157. supervisely/project/pointcloud_episode_project.py +37 -8
  158. supervisely/project/pointcloud_project.py +30 -2
  159. supervisely/project/project.py +14 -2
  160. supervisely/project/project_meta.py +27 -1
  161. supervisely/project/project_settings.py +32 -18
  162. supervisely/project/versioning/__init__.py +1 -0
  163. supervisely/project/versioning/common.py +20 -0
  164. supervisely/project/versioning/schema_fields.py +35 -0
  165. supervisely/project/versioning/video_schema.py +221 -0
  166. supervisely/project/versioning/volume_schema.py +87 -0
  167. supervisely/project/video_project.py +717 -15
  168. supervisely/project/volume_project.py +623 -5
  169. supervisely/template/experiment/experiment.html.jinja +4 -4
  170. supervisely/template/experiment/experiment_generator.py +14 -21
  171. supervisely/template/live_training/__init__.py +0 -0
  172. supervisely/template/live_training/header.html.jinja +96 -0
  173. supervisely/template/live_training/live_training.html.jinja +51 -0
  174. supervisely/template/live_training/live_training_generator.py +464 -0
  175. supervisely/template/live_training/sly-style.css +402 -0
  176. supervisely/template/live_training/template.html.jinja +18 -0
  177. supervisely/versions.json +28 -26
  178. supervisely/video/sampling.py +39 -20
  179. supervisely/video/video.py +40 -11
  180. supervisely/video_annotation/video_object.py +29 -4
  181. supervisely/volume/stl_converter.py +2 -0
  182. supervisely/worker_api/agent_rpc.py +24 -1
  183. supervisely/worker_api/rpc_servicer.py +31 -7
  184. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/METADATA +56 -39
  185. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/RECORD +189 -142
  186. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
  187. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
  188. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
  189. {supervisely-6.73.452.dist-info → supervisely-6.73.513.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,239 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, List, Optional
4
+
5
+ from supervisely.app import DataJson
6
+ from supervisely.app.widgets import Widget
7
+
8
+ try:
9
+ from typing import Literal
10
+ except ImportError:
11
+ from typing_extensions import Literal
12
+
13
+
14
+ class ActivityFeed(Widget):
15
+ """ActivityFeed is a widget that displays a vertical list of activity items with status indicators.
16
+ Similar to a timeline or activity log showing sequential events with their current status.
17
+
18
+ Each item can contain a custom widget as content and displays a status indicator (pending, in process, completed, failed).
19
+ Items are automatically numbered if no number is provided.
20
+
21
+ Read about it in `Developer Portal <https://developer.supervisely.com/app-development/widgets/layouts-and-containers/activity-feed>`_
22
+ (including screenshots and examples).
23
+
24
+ :param items: List of ActivityFeed.Item objects to display
25
+ :type items: Optional[List[ActivityFeed.Item]]
26
+ :param widget_id: An identifier of the widget.
27
+ :type widget_id: str, optional
28
+
29
+ :Usage example:
30
+ .. code-block:: python
31
+
32
+ from supervisely.app.widgets import ActivityFeed, Text
33
+
34
+ # Create items with custom content
35
+ item1 = ActivityFeed.Item(
36
+ content=Text("Processing dataset"),
37
+ status="completed"
38
+ )
39
+ item2 = ActivityFeed.Item(
40
+ content=Text("Training model"),
41
+ status="in_progress",
42
+ number=2
43
+ )
44
+ item3 = ActivityFeed.Item(
45
+ content=Text("Generating report"),
46
+ status="pending"
47
+ )
48
+
49
+ # Create activity feed
50
+ feed = ActivityFeed(items=[item1, item2, item3])
51
+
52
+ # Add item during runtime
53
+ new_item = ActivityFeed.Item(
54
+ content=Text("Deploy model"),
55
+ status="pending"
56
+ )
57
+ feed.add_item(new_item)
58
+
59
+ # Update status by item number
60
+ feed.set_status(2, "completed")
61
+
62
+ # Get item status
63
+ status = feed.get_status(2)
64
+ """
65
+
66
+ class Item:
67
+ """Represents a single item in the ActivityFeed.
68
+
69
+ :param content: Widget to display as the item content
70
+ :type content: Widget
71
+ :param status: Status of the item (pending, in_progress, completed, failed)
72
+ :type status: Literal["pending", "in_progress", "completed", "failed"]
73
+ :param number: Position number in the feed (auto-assigned if not provided)
74
+ :type number: Optional[int]
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ content: Widget,
80
+ status: Literal["pending", "in_progress", "completed", "failed"] = "pending",
81
+ number: Optional[int] = None,
82
+ ) -> ActivityFeed.Item:
83
+ self.content = content
84
+ self.status = status
85
+ self.number = number
86
+ self._validate_status()
87
+
88
+ def _validate_status(self):
89
+ valid_statuses = ["pending", "in_progress", "completed", "failed"]
90
+ if self.status not in valid_statuses:
91
+ raise ValueError(
92
+ f"Invalid status '{self.status}'. Must be one of: {', '.join(valid_statuses)}"
93
+ )
94
+
95
+ def to_json(self):
96
+ return {
97
+ "number": self.number,
98
+ "status": self.status,
99
+ }
100
+
101
+ def __init__(
102
+ self,
103
+ items: Optional[List[ActivityFeed.Item]] = None,
104
+ widget_id: Optional[str] = None,
105
+ ):
106
+ self._items = items if items is not None else []
107
+ self._auto_assign_numbers()
108
+ super().__init__(widget_id=widget_id, file_path=__file__)
109
+
110
+ def _auto_assign_numbers(self):
111
+ """Automatically assign numbers to items that don't have them."""
112
+ next_number = 1
113
+ for item in self._items:
114
+ if item.number is None:
115
+ item.number = next_number
116
+ next_number = max(next_number, item.number) + 1
117
+
118
+ def get_json_data(self) -> Dict:
119
+ """Returns dictionary with widget data.
120
+
121
+ :return: Dictionary with items data
122
+ :rtype: Dict
123
+ """
124
+ return {
125
+ "items": [item.to_json() for item in self._items],
126
+ }
127
+
128
+ def get_json_state(self) -> Dict:
129
+ """Returns dictionary with widget state (empty for this widget).
130
+
131
+ :return: Empty dictionary
132
+ :rtype: Dict
133
+ """
134
+ return {}
135
+
136
+ def add_item(
137
+ self,
138
+ item: Optional[ActivityFeed.Item] = None,
139
+ content: Optional[Widget] = None,
140
+ status: Literal["pending", "in_progress", "completed", "failed"] = "pending",
141
+ number: Optional[int] = None,
142
+ ) -> None:
143
+ """Add a new item to the activity feed.
144
+
145
+ You can either pass an ActivityFeed.Item object or provide content and status separately.
146
+
147
+ :param item: ActivityFeed.Item to add
148
+ :type item: Optional[ActivityFeed.Item]
149
+ :param content: Widget content (used if item is not provided)
150
+ :type content: Optional[Widget]
151
+ :param status: Status of the item (used if item is not provided)
152
+ :type status: Literal["pending", "in_progress", "completed", "failed"]
153
+ :param number: Position number (auto-assigned if not provided)
154
+ :type number: Optional[int]
155
+ """
156
+ if item is None:
157
+ if content is None:
158
+ raise ValueError("Either 'item' or 'content' must be provided")
159
+ item = ActivityFeed.Item(content=content, status=status, number=number)
160
+
161
+ if item.number is None:
162
+ # Auto-assign number
163
+ if self._items:
164
+ item.number = max(i.number for i in self._items) + 1
165
+ else:
166
+ item.number = 1
167
+
168
+ self._items.append(item)
169
+ self.update_data()
170
+ DataJson().send_changes()
171
+
172
+ def remove_item(self, number: int) -> None:
173
+ """Remove an item from the activity feed by its number.
174
+
175
+ :param number: Number of the item to remove. Starts from 1.
176
+ :type number: int
177
+ """
178
+ self._items = [item for item in self._items if item.number != number]
179
+ self.update_data()
180
+ DataJson().send_changes()
181
+
182
+ def set_status(
183
+ self,
184
+ number: int,
185
+ status: Literal["pending", "in_progress", "completed", "failed"],
186
+ ) -> None:
187
+ """Update the status of an item by its number.
188
+
189
+ :param number: Number of the item to update. Starts from 1.
190
+ :type number: int
191
+ :param status: New status for the item
192
+ :type status: Literal["pending", "in_progress", "completed", "failed"]
193
+ """
194
+ for i, item in enumerate(self._items):
195
+ if item.number == number:
196
+ item.status = status
197
+ item._validate_status()
198
+ DataJson()[self.widget_id]["items"][i]["status"] = status
199
+ DataJson().send_changes()
200
+ return
201
+ raise ValueError(f"Item with number {number} not found")
202
+
203
+ def get_status(self, number: int) -> str:
204
+ """Get the status of an item by its number.
205
+
206
+ :param number: Number of the item. Starts from 1.
207
+ :type number: int
208
+ :return: Status of the item
209
+ :rtype: str
210
+ """
211
+ for item in self._items:
212
+ if item.number == number:
213
+ return item.status
214
+ raise ValueError(f"Item with number {number} not found")
215
+
216
+ def get_items(self) -> List[ActivityFeed.Item]:
217
+ """Get all items in the activity feed.
218
+
219
+ :return: List of all items
220
+ :rtype: List[ActivityFeed.Item]
221
+ """
222
+ return self._items
223
+
224
+ def clear(self) -> None:
225
+ """Remove all items from the activity feed."""
226
+ self._items = []
227
+ self.update_data()
228
+ DataJson().send_changes()
229
+
230
+ def set_items(self, items: List[ActivityFeed.Item]) -> None:
231
+ """Replace all items in the activity feed.
232
+
233
+ :param items: New list of items
234
+ :type items: List[ActivityFeed.Item]
235
+ """
236
+ self._items = items
237
+ self._auto_assign_numbers()
238
+ self.update_data()
239
+ DataJson().send_changes()
@@ -0,0 +1,78 @@
1
+ .sly-activity-feed {
2
+ padding: 10px;
3
+ }
4
+
5
+ .sly-activity-feed-item {
6
+ display: flex;
7
+ position: relative;
8
+ padding-bottom: 20px;
9
+ }
10
+
11
+ .sly-activity-feed-item:last-child {
12
+ padding-bottom: 0;
13
+ }
14
+
15
+ .sly-activity-feed-item:not(:last-child) .sly-activity-feed-line {
16
+ display: block;
17
+ }
18
+
19
+ .sly-activity-feed-marker {
20
+ flex-shrink: 0;
21
+ width: 40px;
22
+ display: flex;
23
+ flex-direction: column;
24
+ align-items: center;
25
+ position: relative;
26
+ }
27
+
28
+ .sly-activity-feed-circle {
29
+ width: 16px;
30
+ height: 16px;
31
+ border-radius: 50%;
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ flex-shrink: 0;
36
+ font-size: 10px;
37
+ color: white;
38
+ z-index: 1;
39
+ }
40
+
41
+ .sly-activity-feed-circle.completed {
42
+ background-color: #67C23A;
43
+ }
44
+
45
+ .sly-activity-feed-circle.failed {
46
+ background-color: #F56C6C;
47
+ }
48
+
49
+ .sly-activity-feed-circle.in_progress {
50
+ background-color: #409EFF;
51
+ }
52
+
53
+ .sly-activity-feed-circle.pending {
54
+ background-color: #909399;
55
+ }
56
+
57
+ .sly-activity-feed-line {
58
+ position: absolute;
59
+ top: 16px;
60
+ left: 50%;
61
+ transform: translateX(-50%);
62
+ width: 2px;
63
+ height: calc(100% + 4px);
64
+ background-color: #E4E7ED;
65
+ display: none;
66
+ }
67
+
68
+ .sly-activity-feed-content {
69
+ flex: 1;
70
+ padding-left: 10px;
71
+ padding-top: 0px;
72
+ }
73
+
74
+ .sly-activity-feed-number {
75
+ font-size: 11px;
76
+ color: #909399;
77
+ margin-top: 4px;
78
+ }
@@ -0,0 +1,22 @@
1
+ <link rel="stylesheet" href="./sly/css/app/widgets/activity_feed/style.css" />
2
+
3
+ <div class="sly-activity-feed">
4
+ {% for item in widget._items %}
5
+ <div class="sly-activity-feed-item" v-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}]">
6
+ <div class="sly-activity-feed-marker">
7
+ <div class="sly-activity-feed-circle" :class="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status">
8
+ <i v-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status === 'completed'" class="el-icon-check"></i>
9
+ <i v-else-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status === 'failed'"
10
+ class="el-icon-close"></i>
11
+ <i v-else-if="data.{{{widget.widget_id}}}.items[{{{loop.index0}}}].status === 'in_progress'"
12
+ class="el-icon-loading"></i>
13
+ <i v-else class="el-icon-time"></i>
14
+ </div>
15
+ <div class="sly-activity-feed-line"></div>
16
+ </div>
17
+ <div class="sly-activity-feed-content">
18
+ {{{item.content}}}
19
+ </div>
20
+ </div>
21
+ {% endfor %}
22
+ </div>
@@ -157,3 +157,23 @@ class Card(Widget):
157
157
  :rtype: bool
158
158
  """
159
159
  return self._disabled["disabled"]
160
+
161
+ @property
162
+ def description(self) -> Optional[str]:
163
+ """Description of the card.
164
+
165
+ :return: Description of the card.
166
+ :rtype: Optional[str]
167
+ """
168
+ return self._description
169
+
170
+ @description.setter
171
+ def description(self, value: str) -> None:
172
+ """Sets the description of the card.
173
+
174
+ :param value: Description of the card.
175
+ :type value: str
176
+ """
177
+ self._description = value
178
+ StateJson()[self.widget_id]["description"] = self._description
179
+ StateJson().send_changes()
@@ -1,23 +1,25 @@
1
- from typing import List, Optional, Union
1
+ from typing import Callable, List, Optional, Union
2
2
 
3
3
  from supervisely import ObjClass, ObjClassCollection
4
- from supervisely.app import StateJson
5
- from supervisely.app.widgets import Button, NotificationBox, Widget, generate_id
4
+ from supervisely.app import DataJson, StateJson
5
+ from supervisely.app.widgets import NotificationBox, Text, Widget
6
+ from supervisely.geometry.alpha_mask import AlphaMask
6
7
  from supervisely.geometry.any_geometry import AnyGeometry
7
8
  from supervisely.geometry.bitmap import Bitmap
8
- from supervisely.geometry.alpha_mask import AlphaMask
9
9
  from supervisely.geometry.closed_surface_mesh import ClosedSurfaceMesh
10
10
  from supervisely.geometry.cuboid_2d import Cuboid2d
11
11
  from supervisely.geometry.cuboid_3d import Cuboid3d
12
12
  from supervisely.geometry.graph import GraphNodes
13
13
  from supervisely.geometry.mask_3d import Mask3D
14
14
  from supervisely.geometry.multichannel_bitmap import MultichannelBitmap
15
+ from supervisely.geometry.oriented_bbox import OrientedBBox
15
16
  from supervisely.geometry.point import Point
16
17
  from supervisely.geometry.point_3d import Point3d
17
18
  from supervisely.geometry.pointcloud import Pointcloud
18
19
  from supervisely.geometry.polygon import Polygon
19
20
  from supervisely.geometry.polyline import Polyline
20
21
  from supervisely.geometry.rectangle import Rectangle
22
+ from supervisely.imaging.color import generate_rgb
21
23
 
22
24
  type_to_shape_text = {
23
25
  AnyGeometry: "any shape",
@@ -29,34 +31,57 @@ type_to_shape_text = {
29
31
  Point: "point",
30
32
  Cuboid2d: "cuboid 2d", #
31
33
  Cuboid3d: "cuboid 3d",
32
- Pointcloud: "pointcloud", # # "zmdi zmdi-border-clear"
34
+ Pointcloud: "pointcloud", # "zmdi zmdi-border-clear"
33
35
  MultichannelBitmap: "n-channel mask", # "zmdi zmdi-collection-item"
34
36
  Point3d: "point 3d", # "zmdi zmdi-select-all"
35
37
  GraphNodes: "keypoints",
36
38
  ClosedSurfaceMesh: "volume (3d mask)",
37
39
  Mask3D: "3d mask",
40
+ OrientedBBox: "oriented bbox",
38
41
  }
39
42
 
43
+ shape_text_to_type = {v: k for k, v in type_to_shape_text.items()}
44
+
45
+ # Geometry types available for creating new classes (excluding GraphNodes)
46
+ available_geometry_types = [
47
+ {"value": "rectangle", "label": "Rectangle"},
48
+ {"value": "polygon", "label": "Polygon"},
49
+ {"value": "bitmap (mask)", "label": "Bitmap (mask)"},
50
+ {"value": "polyline", "label": "Polyline"},
51
+ {"value": "point", "label": "Point"},
52
+ {"value": "any shape", "label": "Any shape"},
53
+ {"value": "oriented bbox", "label": "Oriented Bounding Box"},
54
+ ]
55
+
40
56
 
41
57
  class ClassesListSelector(Widget):
42
58
  class Routes:
43
59
  CHECKBOX_CHANGED = "checkbox_cb"
60
+ CLASS_CREATED = "class_created_cb"
44
61
 
45
62
  def __init__(
46
63
  self,
47
64
  classes: Optional[Union[List[ObjClass], ObjClassCollection]] = [],
48
65
  multiple: Optional[bool] = False,
49
66
  empty_notification: Optional[NotificationBox] = None,
67
+ allow_new_classes: Optional[bool] = False,
50
68
  widget_id: Optional[str] = None,
51
69
  ):
52
- self._classes = classes
70
+ # Convert to list for internal use to allow mutations when adding new classes
71
+ if isinstance(classes, ObjClassCollection):
72
+ self._classes = list(classes)
73
+ else:
74
+ self._classes = list(classes) if classes else []
53
75
  self._multiple = multiple
76
+ self._allow_new_classes = allow_new_classes
77
+ self._class_created_handled = False
54
78
  if empty_notification is None:
55
79
  empty_notification = NotificationBox(
56
80
  title="No classes",
57
81
  description="No classes to select.",
58
82
  )
59
83
  self.empty_notification = empty_notification
84
+ self._error_message = Text("", status="error", font_size=13)
60
85
  super().__init__(widget_id=widget_id, file_path=__file__)
61
86
 
62
87
  def get_json_data(self):
@@ -65,14 +90,29 @@ class ClassesListSelector(Widget):
65
90
  shape_text = type_to_shape_text.get(cls.geometry_type)
66
91
  class_dict = {**cls.to_json(), "shape_text": shape_text.upper() if shape_text else ""}
67
92
  classes_list.append(class_dict)
68
- return {"classes": classes_list}
93
+ return {
94
+ "classes": classes_list,
95
+ "availableGeometryTypes": available_geometry_types,
96
+ }
69
97
 
70
98
  def get_json_state(self):
71
- return {"selected": [False for _ in self._classes]}
99
+ return {
100
+ "selected": [False for _ in self._classes],
101
+ "createClassDialog": {
102
+ "visible": False,
103
+ "className": "",
104
+ "geometryType": "rectangle",
105
+ "showError": False,
106
+ },
107
+ }
72
108
 
73
109
  def set(self, classes: Union[List[ObjClass], ObjClassCollection]):
74
110
  selected_classes = [cls.name for cls in self.get_selected_classes()]
75
- self._classes = classes
111
+ # Convert to list for internal use
112
+ if isinstance(classes, ObjClassCollection):
113
+ self._classes = list(classes)
114
+ else:
115
+ self._classes = list(classes) if classes else []
76
116
  StateJson()[self.widget_id]["selected"] = [
77
117
  cls.name in selected_classes for cls in self._classes
78
118
  ]
@@ -110,6 +150,33 @@ class ClassesListSelector(Widget):
110
150
  def get_all_classes(self):
111
151
  return self._classes
112
152
 
153
+ def _show_error(self, message: str):
154
+ """Show error message in the create class dialog."""
155
+ self._error_message.text = message
156
+ StateJson()[self.widget_id]["createClassDialog"]["showError"] = True
157
+ StateJson().send_changes()
158
+
159
+ def _hide_dialog(self):
160
+ """Hide the create class dialog and reset its state."""
161
+ state_obj = StateJson()[self.widget_id]["createClassDialog"]
162
+ state_obj["visible"] = False
163
+ state_obj["className"] = ""
164
+ state_obj["showError"] = False
165
+ StateJson().send_changes()
166
+
167
+ def _add_new_class(self, new_class: ObjClass):
168
+ """Add a new class to the widget and update the UI."""
169
+ # Add to classes list
170
+ self._classes.append(new_class)
171
+
172
+ # Add selection state for the new class (selected by default)
173
+ StateJson()[self.widget_id]["selected"].append(True)
174
+
175
+ # Update data to reflect the new class in the UI
176
+ self.update_data()
177
+ DataJson().send_changes()
178
+ StateJson().send_changes()
179
+
113
180
  def selection_changed(self, func):
114
181
  route_path = self.get_route_path(ClassesListSelector.Routes.CHECKBOX_CHANGED)
115
182
  server = self._sly_app.get_server()
@@ -121,3 +188,48 @@ class ClassesListSelector(Widget):
121
188
  func(selected)
122
189
 
123
190
  return _click
191
+
192
+ def class_created(self, func: Callable[[ObjClass], None]):
193
+ """
194
+ Decorator to handle new class creation event.
195
+ The decorated function receives the newly created ObjClass.
196
+
197
+ :param func: Function to be called when a new class is created
198
+ :type func: Callable[[ObjClass], None]
199
+ """
200
+ route_path = self.get_route_path(ClassesListSelector.Routes.CLASS_CREATED)
201
+ server = self._sly_app.get_server()
202
+ self._class_created_handled = True
203
+
204
+ @server.post(route_path)
205
+ def _class_created():
206
+ state = StateJson()[self.widget_id]["createClassDialog"]
207
+ class_name = state["className"].strip()
208
+ geometry_type_str = state["geometryType"]
209
+
210
+ if not class_name:
211
+ self._show_error("Class name cannot be empty")
212
+ return
213
+
214
+ if any(cls.name == class_name for cls in self._classes):
215
+ self._show_error(f"Class '{class_name}' already exists")
216
+ return
217
+
218
+ geometry_type = shape_text_to_type.get(geometry_type_str)
219
+ if geometry_type is None:
220
+ self._show_error("Invalid geometry type")
221
+ return
222
+
223
+ # Generate color for the new class
224
+ existing_colors = [cls.color for cls in self._classes]
225
+ new_color = generate_rgb(existing_colors)
226
+
227
+ # Create new class
228
+ new_class = ObjClass(name=class_name, geometry_type=geometry_type, color=new_color)
229
+
230
+ self._add_new_class(new_class)
231
+ self._hide_dialog()
232
+
233
+ func(new_class)
234
+
235
+ return _class_created