Show LabelBOT popup despite no suggestions - #1478
Conversation
There was a problem hiding this comment.
Pull request overview
This PR changes LabelBOT’s empty-result behavior so annotations can still be created when no suggestions are found, then prompts the user to manually choose a label or dismiss the temporary annotation.
Changes:
- Backend LabelBOT now returns an empty label list instead of throwing a 404.
- Image/video annotation creation can save annotations without immediately attaching a label.
- LabelBOT popup now displays an empty-suggestions message and supports manual label selection/deletion flow.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
app/Services/LabelBot/LabelBotService.php |
Returns an empty suggestion array when no similar labels are found. |
app/Http/Controllers/Api/ImageAnnotationController.php |
Skips label authorization/attachment when no label is available. |
app/Http/Controllers/Api/VideoAnnotationController.php |
Mirrors the image annotation empty-label persistence behavior. |
resources/assets/js/annotations/components/labelbotPopup.vue |
Adds no-suggestions UI and temporary annotation deletion behavior. |
resources/assets/js/annotations/annotatorContainer.vue |
Refreshes annotations after manually attaching a label when no previous label existed. |
resources/assets/sass/annotations/components/_labelbot-popup.scss |
Adds styling for the no-suggestions popup message. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (!this.selectedLabel || this.selectedLabel.id !== label.id) { | ||
| this.dismissLabelbotAnnotation = false; | ||
| this.$emit('update', {label: label, annotation: this.annotation}); |
| if (this.noLabels && this.dismissLabelbotAnnotation) { | ||
| this.deleteLabelAnnotation(); |
|
|
||
| if (empty($topNLabels)) { | ||
| throw new NotFoundHttpException("LabelBOT could not find similar annotations."); | ||
| return []; |
| $response->assertSuccessful(); | ||
|
|
||
| // We expect an empty array | ||
| $response->assertJsonFragment([]); |
| this.emitClose(); | ||
| Events.emit('labelbot.chose_label_1'); | ||
| if (this.noLabels) { | ||
| this.deleteLabelAnnotation(); |
| if (this.noLabels && this.deletePendingLabelbotAnnotation) { | ||
| this.deleteLabelAnnotation(); |
| if ($label) { | ||
| VideoAnnotationLabel::create([ | ||
| 'label_id' => $label->id, | ||
| 'user_id' => $request->user()->id, | ||
| 'annotation_id' => $annotation->id, | ||
| ]); | ||
| } |
|
Please request my review again once you are through with the AI. |
|
@mzur While trying to implement this for videos, I found some other issues:
Screencast.from.2026-06-02.13-58-18.webm
Screencast.from.2026-06-02.14-04-56.webmOr was that intentional for 2 ? Otherwise, I could try to fix it here. |
207345e to
ac0a62a
Compare
|
No unlabeled annotations will be saved to the database anymore. The OpenLayers feature element is now temporarily attached to the annotation element. If LabelBOT returns no results, the feature element ID is used as the popup ID (key), and its geometry is used for popup placement. For video annotations, the pending annotation is attached in the same way. The two issues mentioned previously should now be fixed. I also noticed that you, @mzur, already opened issue #1510 regarding the video screen crash. Another issue I found was related to tracking annotations. When creating an ROI in a video, then seeking to another time, and finally clicking "Finish and Track Annotation" while LabelBOT is enabled, the video screen crashes. This happens because there are no available annotations to use as an anchor at the new current time: Screencast.from.2026-07-31.12-17-16.webmThis was already fixed for non-tracking annotations, and I have now applied the same fix for tracking annotations as well. When clicking the play button while a LabelBOT popup is open, the popup remains visible until the video is paused or the user interacts with the popup. If the popup is empty, pausing the video will remove the entire pending annotation if no label has been entered. If the popup is not empty, the Top-1 is already saved. |
mzur
left a comment
There was a problem hiding this comment.
I merged the current master so the code works with my new DINO ONNX (generated with a newer Torch version).
I think the popup should close when there are no labels and the user clicks play. It closes anyway once the user clicks pause, so we can just close it earlier.
If the popup timeout is enabled, two Esc presses are needed to close the popup. It should close right away.
Maybe the whole logic of remove callbacks, direct calls to the OpenLayers source and delete-pending events can be consolidated a little more. They all do similar things but it feels very inconsistent.
There are several missing semicolons in the code. Please rerun the AI review in the next iteration before requesting my own review.
| list-style-type: none; | ||
| max-width: 300px; | ||
| margin: 0; | ||
| padding: $padding-small-vertical; | ||
| background-color: $body-bg; | ||
| color: $text-muted; |
There was a problem hiding this comment.
Most is already handled by .labelbot-labels.
| list-style-type: none; | |
| max-width: 300px; | |
| margin: 0; | |
| padding: $padding-small-vertical; | |
| background-color: $body-bg; | |
| color: $text-muted; | |
| padding: $padding-small-vertical; | |
| color: $text-muted; |
| let annotationFeature; | ||
| if (this.noLabels) { | ||
| annotationFeature = this.annotation.feature; | ||
| this.pendingAnnotation = true; |
There was a problem hiding this comment.
This case should automatically focus the label typeahead when the popup opens.
| pendingAnnotation.feature = undefined; | ||
| delete pendingAnnotation.feature; | ||
|
|
||
| const track = pendingAnnotation.track |
There was a problem hiding this comment.
Tracking video annotations is broken (and was before this PR) when LabelBOT is not enabled or did not return results. The track attribute is not passed to newAnnotation below.
| // in case LabelBOT returns no results | ||
| newAnnotation.feature = pendingAnnotationFeature; | ||
| newAnnotation.pendingAnnotation = pendingAnnotation; | ||
| newAnnotation.track = track; |
There was a problem hiding this comment.
This can be removed when the track attribute is added to newAnnotation above.
| newAnnotation.track = track; |
| return annotation; | ||
| }); | ||
| .then((response) => { | ||
| if (response.status === 204) return null; |
There was a problem hiding this comment.
Please add a comment what this case means.
Thinking about this, maybe we should not use 204 after all, because it signals some kind of success. Maybe we should use 422 with a special error message for the feature_vector and then catch this and check the message here? A 422 will have better semantics here too because the promise chain will be skipped on an error response.
| this.lineFeature.set('unselectable', true); | ||
| this.lineFeature.set('color', this.labels[0].color); | ||
| if (this.noLabels) { | ||
| this.lineFeature.set('color', '5bc0de'); |
There was a problem hiding this comment.
Add a comment that this is the "info" color.
| e.feature.set('color', '5bc0de'); | ||
| e.feature.setStyle(Styles.editing); | ||
|
|
||
| newAnnotation.feature = e.feature, // we need this temporarily if LabelBOT returns no results |
There was a problem hiding this comment.
| newAnnotation.feature = e.feature, // we need this temporarily if LabelBOT returns no results | |
| newAnnotation.feature = e.feature; // we need this temporarily if LabelBOT returns no results |
| shouldHaveProgressBar: true, | ||
| maybeGetsAttention: false, | ||
| typeaheadFocused: false, | ||
| pendingAnnotation: false, |
There was a problem hiding this comment.
Better use:
| pendingAnnotation: false, | |
| isPendingAnnotation: false, |
| // Query an empty DB | ||
| $response = $this->post("/api/v1/images/{$this->image->id}/annotations", [ | ||
| 'shape_id' => Shape::pointId(), | ||
| 'feature_vector' => range(1, 384), | ||
| 'confidence' => 0.5, | ||
| 'points' => [10, 11], | ||
| ]); | ||
| // We expect no content | ||
| $response->assertStatus(204); | ||
|
|
There was a problem hiding this comment.
This should be added as a separate test case.
| // Query an empty DB | ||
| $response = $this->json('POST', "api/v1/videos/{$this->video->id}/annotations", [ | ||
| 'shape_id' => Shape::pointId(), | ||
| 'feature_vector' => range(1, 384), | ||
| 'points' => [[10, 11]], | ||
| 'frames' => [0.0], | ||
| ]); | ||
| // We expect no content | ||
| $response->assertStatus(204); | ||
|
|
When LabelBOT returns no suggestions, an empty array will be sent to the front end. The LabelBOT popup will show a message to type a label in the typeahead. For now the temp annotation will be saved in the database without a label and it will have the editing style in the front end until it's confirmed with a label, if not then it will be deleted.