Skip to content

Conversation

@ishajagadish
Copy link
Collaborator

Resolves #4035

Added an “AI-generated” icon and tooltip across gallery, admin/label map, and validate views. The frontend uses new AiLabelIndicator helpers and CSS tweaks so the icon sits on the marker corner, and a tooltip explains the label was AI-generated. On the backend, LabelTable.getRecentLabelsMetadata flags ai_generated via a SQL EXISTS check on labels placed by users with the AI role, and that flag is exposed through LabelFormats/AdminController to drive the UI.

Before/After screenshots

Before:
Screenshot 2025-12-11 at 2 19 36 PM

After:
Screenshot 2025-12-11 at 2 18 55 PM

Testing instructions
  1. Navigate to any of the following pages: /validate, /expertValidate, /mobileValidate, /gallery, /labelMap, /admin (the "Map" tab), /admin (the "Labels" tab), /admin (the "Label Search" tab), /admin/label.
  2. Notice the new AI icon on any labels that were AI-generated.
  3. Hover over this label to see the tooltip message.
Things to check before submitting the PR
  • I've written a descriptive PR title.
  • I've added/updated comments for large or confusing blocks of code.
  • I've included before/after screenshots above.
  • I've asked for and included translations for any user facing text that was added or modified.
  • I've updated any logging. Clicks, keyboard presses, and other user interactions should be logged. If you're not sure how (or if you need to update the logging), ask Mikey. Then make sure the documentation on this wiki page is up to date for the logs you added/updated.
  • I've tested on mobile (only needed for validation page).

…logic to check SQL tables. Added a tooltip message that pops up on hover across gallery, admin, and validate pages.
Copy link
Member

@misaugstad misaugstad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, this looks incredibly solid, great work!! Some comments:

  1. Looks like when switching out the AI icon, something was missed on the /admin/label/ search page:
    image
  2. There are references to admin-ai-icon-header... Are those remnants of when you had an AI icon next to the label type name in the popup headers? If so, there's some stuff left to clean out there! If not, what does it do?
  3. I didn't have a chance to carefully look through the three AiLabelIndicator.js files, but I assume that they have quite a bit of overlap. Do you think that it makes sense to try to consolidate them? We can talk in our meeting about how they're differentiated.
  4. There are some more comments in the code, including some about writing more efficient queries!

at.stale,
comment.comments
comment.comments,
EXISTS (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that, instead of adding a subquery here, it would be more efficient to just do the joins in the main query. So within the query it would look like

...
INNER JOIN sidewalk_user AS u ON at.user_id = u.user_id
INNER JOIN user_role AS ur ON u.user_id = ur.user_id
INNER JOIN role AS r ON ur.role_id = r.role_id
INNER JOIN label_point AS lp ON lb1.label_id = lp.label_id
...

And then you could just refer to it as r.role = 'AI' rather than doing the subquery in the SELECT statement.

This comment was marked as resolved.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@misaugstad I removed the per-row EXISTS subquery in getRecentLabelsMetadata and pulled the role check into the main query: we now join user_role/role once and select (ai_user.user_id IS NOT NULL) AS ai_generated, using r.role = 'AI' from the join instead of a subquery.

if (includeAiTags) la.flatMap(_.tags).getOrElse(List.empty[String].bind).asColumnOf[Option[List[String]]]
else None.asInstanceOf[Option[List[String]]].asColumnOf[Option[List[String]]]
else None.asInstanceOf[Option[List[String]]].asColumnOf[Option[List[String]]],
isAiUser(l.userId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same comment above applies to these Slick queries as well. Rather than having this function that essentially creates a subquery for each row, we can do the joins above:

val _labelInfo = for {
      (_lb, _at, _us) <- labelsWithAuditTasksAndUserStats
      _ur <- userRoles if _us.userId === _ur.userId
      _r <- roleTable if _ur.roleId === _r.roleId
      ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would apply this to all the queries that you have, even if I didn't explicitly comment on them.

aiIcon.setAttribute('data-toggle', 'tooltip');
aiIcon.setAttribute('data-placement', 'top');
aiIcon.setAttribute('title', i18next.t('common:ai-disclaimer', { aiVal: aiValidation }));
aiIcon.setAttribute('title', i18next.t('common:ai-generated-label-tooltip'));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't test this change, but I'm assuming it's a mistake?

… fixed /admin/label/search icon issue, cleaned up code.
Copy link
Member

@misaugstad misaugstad left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking at LabelMap, and this is not what the table was supposed to look like:
image

It used to look like this
image

Various comments throughout the code as well!

at.stale,
comment.comments
comment.comments,
EXISTS (

This comment was marked as resolved.

}

function setLabel(labelMetadata) {
const isAiGenerated = Boolean(labelMetadata['ai_generated']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that we should be using Boolean() here... I think that it's meant to coerce anything to a boolean primitive. But if our API is returning something other than a boolean primitive, then we have bigger problems and we shouldn't be hiding it like this.

But that's just based on my cursory googling, since I've never used the Boolean() function before. Did you have a reason for using it..?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@misaugstad I used Boolean() just to normalize potential null/undefined to a boolean when the field was missing, not because I expected non-boolean values. I agree we shouldn’t mask bad data - happy to drop the wrapper and rely on the API returning a real boolean. Let me switch it to use the value directly.

…ltip z-index, added comments, reordered AiLabelIndicator.js
self.panorama.setPano(labelMetadata['gsv_panorama_id'], labelMetadata['heading'],
labelMetadata['pitch'], labelMetadata['zoom'], panoCallback);

const isAiGenerated = Boolean(labelMetadata['ai_generated']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You missed that you used Boolean() here. You should just be able to pass in labelMetadata['ai_generated'] directly to AdminPanoramaLabel() like we do for all the other parameters (you shouldn't need to check if it's equal to true either).

column-gap: 10px;
}

.gallery-expanded-view-header-label {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This CSS class isn't used anywhere, is it? I think that since you removed the icon from the header, all the edits from this file can likely be removed?

.ai-icon-marker-expanded {
top: -6px;
left: -4px;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aaand just missing the newline here. Most minor thing in the world lol. But I would strongly recommend looking through the "Files changed" section of any PR before asking for reviews, since that's what the reviewers will be looking at! Will help you catch all sorts of mistakes

setProperty("newTags", [...params.tags]); // Copy tags to newTags.
}
if ("ai_tags" in params) setAuditProperty("aiTags", params.ai_tags);
if ("ai_generated" in params) setAuditProperty("aiGenerated", Boolean(params.ai_generated));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still using Boolean() here...

@misaugstad
Copy link
Member

@ishajagadish I've fixed the issue with the inner joins.

  • For the raw SQL query, the inner join worked as I had expected
  • For the Slick queries, for some reason Slick was generating a cross join when we tried to do the inner join at the place where we were. I was able to get around this by passing along _r.role === 'AI' instead of passing through the whole table. Not totally sure why that prevents Slick from creating a cross join that we don't need, but it worked!

I've fixed this in all four queries (Gallery, Validate, LabelMap, and the label popup).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add AI generated label indicator

3 participants