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.
- supervisely/__init__.py +137 -1
- supervisely/_utils.py +81 -0
- supervisely/annotation/annotation.py +8 -2
- supervisely/annotation/json_geometries_map.py +14 -11
- supervisely/annotation/label.py +80 -3
- supervisely/api/annotation_api.py +14 -11
- supervisely/api/api.py +59 -38
- supervisely/api/app_api.py +11 -2
- supervisely/api/dataset_api.py +74 -12
- supervisely/api/entities_collection_api.py +10 -0
- supervisely/api/entity_annotation/figure_api.py +52 -4
- supervisely/api/entity_annotation/object_api.py +3 -3
- supervisely/api/entity_annotation/tag_api.py +63 -12
- supervisely/api/guides_api.py +210 -0
- supervisely/api/image_api.py +72 -1
- supervisely/api/labeling_job_api.py +83 -1
- supervisely/api/labeling_queue_api.py +33 -7
- supervisely/api/module_api.py +9 -0
- supervisely/api/project_api.py +71 -26
- supervisely/api/storage_api.py +3 -1
- supervisely/api/task_api.py +13 -2
- supervisely/api/team_api.py +4 -3
- supervisely/api/video/video_annotation_api.py +119 -3
- supervisely/api/video/video_api.py +65 -14
- supervisely/api/video/video_figure_api.py +24 -11
- supervisely/app/__init__.py +1 -1
- supervisely/app/content.py +23 -7
- supervisely/app/development/development.py +18 -2
- supervisely/app/fastapi/__init__.py +1 -0
- supervisely/app/fastapi/custom_static_files.py +1 -1
- supervisely/app/fastapi/multi_user.py +105 -0
- supervisely/app/fastapi/subapp.py +88 -42
- supervisely/app/fastapi/websocket.py +77 -9
- supervisely/app/singleton.py +21 -0
- supervisely/app/v1/app_service.py +18 -2
- supervisely/app/v1/constants.py +7 -1
- supervisely/app/widgets/__init__.py +6 -0
- supervisely/app/widgets/activity_feed/__init__.py +0 -0
- supervisely/app/widgets/activity_feed/activity_feed.py +239 -0
- supervisely/app/widgets/activity_feed/style.css +78 -0
- supervisely/app/widgets/activity_feed/template.html +22 -0
- supervisely/app/widgets/card/card.py +20 -0
- supervisely/app/widgets/classes_list_selector/classes_list_selector.py +121 -9
- supervisely/app/widgets/classes_list_selector/template.html +60 -93
- supervisely/app/widgets/classes_mapping/classes_mapping.py +13 -12
- supervisely/app/widgets/classes_table/classes_table.py +1 -0
- supervisely/app/widgets/deploy_model/deploy_model.py +56 -35
- supervisely/app/widgets/dialog/dialog.py +12 -0
- supervisely/app/widgets/dialog/template.html +2 -1
- supervisely/app/widgets/ecosystem_model_selector/ecosystem_model_selector.py +1 -1
- supervisely/app/widgets/experiment_selector/experiment_selector.py +8 -0
- supervisely/app/widgets/fast_table/fast_table.py +184 -60
- supervisely/app/widgets/fast_table/template.html +1 -1
- supervisely/app/widgets/heatmap/__init__.py +0 -0
- supervisely/app/widgets/heatmap/heatmap.py +564 -0
- supervisely/app/widgets/heatmap/script.js +533 -0
- supervisely/app/widgets/heatmap/style.css +233 -0
- supervisely/app/widgets/heatmap/template.html +21 -0
- supervisely/app/widgets/modal/__init__.py +0 -0
- supervisely/app/widgets/modal/modal.py +198 -0
- supervisely/app/widgets/modal/template.html +10 -0
- supervisely/app/widgets/object_class_view/object_class_view.py +3 -0
- supervisely/app/widgets/radio_tabs/radio_tabs.py +18 -2
- supervisely/app/widgets/radio_tabs/template.html +1 -0
- supervisely/app/widgets/select/select.py +6 -3
- supervisely/app/widgets/select_class/__init__.py +0 -0
- supervisely/app/widgets/select_class/select_class.py +363 -0
- supervisely/app/widgets/select_class/template.html +50 -0
- supervisely/app/widgets/select_cuda/select_cuda.py +22 -0
- supervisely/app/widgets/select_dataset_tree/select_dataset_tree.py +65 -7
- supervisely/app/widgets/select_tag/__init__.py +0 -0
- supervisely/app/widgets/select_tag/select_tag.py +352 -0
- supervisely/app/widgets/select_tag/template.html +64 -0
- supervisely/app/widgets/select_team/select_team.py +37 -4
- supervisely/app/widgets/select_team/template.html +4 -5
- supervisely/app/widgets/select_user/__init__.py +0 -0
- supervisely/app/widgets/select_user/select_user.py +270 -0
- supervisely/app/widgets/select_user/template.html +13 -0
- supervisely/app/widgets/select_workspace/select_workspace.py +59 -10
- supervisely/app/widgets/select_workspace/template.html +9 -12
- supervisely/app/widgets/table/table.py +68 -13
- supervisely/app/widgets/tree_select/tree_select.py +2 -0
- supervisely/aug/aug.py +6 -2
- supervisely/convert/base_converter.py +1 -0
- supervisely/convert/converter.py +2 -2
- supervisely/convert/image/csv/csv_converter.py +24 -15
- supervisely/convert/image/image_converter.py +3 -1
- supervisely/convert/image/image_helper.py +48 -4
- supervisely/convert/image/label_studio/label_studio_converter.py +2 -0
- supervisely/convert/image/medical2d/medical2d_helper.py +2 -24
- supervisely/convert/image/multispectral/multispectral_converter.py +6 -0
- supervisely/convert/image/pascal_voc/pascal_voc_converter.py +8 -5
- supervisely/convert/image/pascal_voc/pascal_voc_helper.py +7 -0
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_converter.py +33 -3
- supervisely/convert/pointcloud/kitti_3d/kitti_3d_helper.py +12 -5
- supervisely/convert/pointcloud/las/las_converter.py +13 -1
- supervisely/convert/pointcloud/las/las_helper.py +110 -11
- supervisely/convert/pointcloud/nuscenes_conv/nuscenes_converter.py +27 -16
- supervisely/convert/pointcloud/pointcloud_converter.py +91 -3
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_converter.py +58 -22
- supervisely/convert/pointcloud_episodes/nuscenes_conv/nuscenes_helper.py +21 -47
- supervisely/convert/video/__init__.py +1 -0
- supervisely/convert/video/multi_view/__init__.py +0 -0
- supervisely/convert/video/multi_view/multi_view.py +543 -0
- supervisely/convert/video/sly/sly_video_converter.py +359 -3
- supervisely/convert/video/video_converter.py +24 -4
- supervisely/convert/volume/dicom/dicom_converter.py +13 -5
- supervisely/convert/volume/dicom/dicom_helper.py +30 -18
- supervisely/geometry/constants.py +1 -0
- supervisely/geometry/geometry.py +4 -0
- supervisely/geometry/helpers.py +5 -1
- supervisely/geometry/oriented_bbox.py +676 -0
- supervisely/geometry/polyline_3d.py +110 -0
- supervisely/geometry/rectangle.py +2 -1
- supervisely/io/env.py +76 -1
- supervisely/io/fs.py +21 -0
- supervisely/nn/benchmark/base_evaluator.py +104 -11
- supervisely/nn/benchmark/instance_segmentation/evaluator.py +1 -8
- supervisely/nn/benchmark/object_detection/evaluator.py +20 -4
- supervisely/nn/benchmark/object_detection/vis_metrics/pr_curve.py +10 -5
- supervisely/nn/benchmark/semantic_segmentation/evaluator.py +34 -16
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/confusion_matrix.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/frequently_confused.py +1 -1
- supervisely/nn/benchmark/semantic_segmentation/vis_metrics/overview.py +1 -1
- supervisely/nn/benchmark/visualization/evaluation_result.py +66 -4
- supervisely/nn/inference/cache.py +43 -18
- supervisely/nn/inference/gui/serving_gui_template.py +5 -2
- supervisely/nn/inference/inference.py +916 -222
- supervisely/nn/inference/inference_request.py +55 -10
- supervisely/nn/inference/predict_app/gui/classes_selector.py +83 -12
- supervisely/nn/inference/predict_app/gui/gui.py +676 -488
- supervisely/nn/inference/predict_app/gui/input_selector.py +205 -26
- supervisely/nn/inference/predict_app/gui/model_selector.py +2 -4
- supervisely/nn/inference/predict_app/gui/output_selector.py +46 -6
- supervisely/nn/inference/predict_app/gui/settings_selector.py +756 -59
- supervisely/nn/inference/predict_app/gui/tags_selector.py +1 -1
- supervisely/nn/inference/predict_app/gui/utils.py +236 -119
- supervisely/nn/inference/predict_app/predict_app.py +2 -2
- supervisely/nn/inference/session.py +43 -35
- supervisely/nn/inference/tracking/bbox_tracking.py +118 -35
- supervisely/nn/inference/tracking/point_tracking.py +5 -1
- supervisely/nn/inference/tracking/tracker_interface.py +10 -1
- supervisely/nn/inference/uploader.py +139 -12
- supervisely/nn/live_training/__init__.py +7 -0
- supervisely/nn/live_training/api_server.py +111 -0
- supervisely/nn/live_training/artifacts_utils.py +243 -0
- supervisely/nn/live_training/checkpoint_utils.py +229 -0
- supervisely/nn/live_training/dynamic_sampler.py +44 -0
- supervisely/nn/live_training/helpers.py +14 -0
- supervisely/nn/live_training/incremental_dataset.py +146 -0
- supervisely/nn/live_training/live_training.py +497 -0
- supervisely/nn/live_training/loss_plateau_detector.py +111 -0
- supervisely/nn/live_training/request_queue.py +52 -0
- supervisely/nn/model/model_api.py +9 -0
- supervisely/nn/model/prediction.py +2 -1
- supervisely/nn/model/prediction_session.py +26 -14
- supervisely/nn/prediction_dto.py +19 -1
- supervisely/nn/tracker/base_tracker.py +11 -1
- supervisely/nn/tracker/botsort/botsort_config.yaml +0 -1
- supervisely/nn/tracker/botsort/tracker/mc_bot_sort.py +7 -4
- supervisely/nn/tracker/botsort_tracker.py +94 -65
- supervisely/nn/tracker/utils.py +4 -5
- supervisely/nn/tracker/visualize.py +93 -93
- supervisely/nn/training/gui/classes_selector.py +16 -1
- supervisely/nn/training/gui/train_val_splits_selector.py +52 -31
- supervisely/nn/training/train_app.py +46 -31
- supervisely/project/data_version.py +115 -51
- supervisely/project/download.py +1 -1
- supervisely/project/pointcloud_episode_project.py +37 -8
- supervisely/project/pointcloud_project.py +30 -2
- supervisely/project/project.py +14 -2
- supervisely/project/project_meta.py +27 -1
- supervisely/project/project_settings.py +32 -18
- supervisely/project/versioning/__init__.py +1 -0
- supervisely/project/versioning/common.py +20 -0
- supervisely/project/versioning/schema_fields.py +35 -0
- supervisely/project/versioning/video_schema.py +221 -0
- supervisely/project/versioning/volume_schema.py +87 -0
- supervisely/project/video_project.py +717 -15
- supervisely/project/volume_project.py +623 -5
- supervisely/template/experiment/experiment.html.jinja +4 -4
- supervisely/template/experiment/experiment_generator.py +14 -21
- supervisely/template/live_training/__init__.py +0 -0
- supervisely/template/live_training/header.html.jinja +96 -0
- supervisely/template/live_training/live_training.html.jinja +51 -0
- supervisely/template/live_training/live_training_generator.py +464 -0
- supervisely/template/live_training/sly-style.css +402 -0
- supervisely/template/live_training/template.html.jinja +18 -0
- supervisely/versions.json +28 -26
- supervisely/video/sampling.py +39 -20
- supervisely/video/video.py +41 -12
- supervisely/video_annotation/video_figure.py +38 -4
- supervisely/video_annotation/video_object.py +29 -4
- supervisely/volume/stl_converter.py +2 -0
- supervisely/worker_api/agent_rpc.py +24 -1
- supervisely/worker_api/rpc_servicer.py +31 -7
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/METADATA +58 -40
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/RECORD +203 -155
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/WHEEL +1 -1
- supervisely_lib/__init__.py +6 -1
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.438.dist-info → supervisely-6.73.513.dist-info/licenses}/LICENSE +0 -0
- {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
|
-
|
|
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]
|
|
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
|
|
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
|