diff --git a/docs/source/guide/webhook_reference.md b/docs/source/guide/webhook_reference.md index 0bdebad2f1e4..080f984d4cb1 100644 --- a/docs/source/guide/webhook_reference.md +++ b/docs/source/guide/webhook_reference.md @@ -129,7 +129,6 @@ The webhook payload includes the name of the action and some additional task dat "created_at": "2021-08-17T13:49:34.326416Z", "updated_at": "2021-08-17T13:49:35.911271Z", "sampling": "Sequential sampling", - "show_ground_truth_first": true, "show_overlap_first": true, "overlap_cohort_percentage": 100, "task_data_login": null, @@ -219,7 +218,6 @@ Sent when a task is deleted from Label Studio. See how to [set up a webhook for "created_at": "2021-08-17T13:49:34.326416Z", "updated_at": "2021-08-17T13:52:09.334425Z", "sampling": "Sequential sampling", - "show_ground_truth_first": true, "show_overlap_first": true, "overlap_cohort_percentage": 100, "task_data_login": null, @@ -328,7 +326,6 @@ The webhook payload includes the name of the action and some additional annotati "created_at": "2021-08-17T13:49:34.326416Z", "updated_at": "2021-08-17T13:52:09.334425Z", "sampling": "Sequential sampling", - "show_ground_truth_first": true, "show_overlap_first": true, "overlap_cohort_percentage": 100, "task_data_login": null, @@ -463,7 +460,6 @@ The webhook payload includes the name of the action and some additional annotati "created_at": "2021-08-12T14:15:01.744507Z", "updated_at": "2021-08-17T13:35:25.697471Z", "sampling": "Sequential sampling", - "show_ground_truth_first": true, "show_overlap_first": true, "overlap_cohort_percentage": 100, "task_data_login": null, @@ -538,7 +534,6 @@ Sent when an annotation is deleted. See how to [set up a webhook for this event] "created_at": "2021-08-17T13:49:34.326416Z", "updated_at": "2021-08-17T13:52:09.334425Z", "sampling": "Sequential sampling", - "show_ground_truth_first": true, "show_overlap_first": true, "overlap_cohort_percentage": 100, "task_data_login": null, @@ -603,7 +598,6 @@ The webhook payload includes the name of the action and some additional project "created_at": "2021-08-17T13:55:58.809065Z", "updated_at": "2021-08-17T13:55:58.809098Z", "sampling": "Sequential sampling", - "show_ground_truth_first": true, "show_overlap_first": true, "overlap_cohort_percentage": 100, "task_data_login": null, @@ -674,7 +668,6 @@ The webhook payload includes the name of the action and some additional project "created_at": "2021-08-12T14:15:01.744507Z", "updated_at": "2021-08-17T13:39:14.054849Z", "sampling": "Sequential sampling", - "show_ground_truth_first": true, "show_overlap_first": true, "overlap_cohort_percentage": 100, "task_data_login": null, diff --git a/label_studio/projects/api.py b/label_studio/projects/api.py index e19760829610..f4cc192bc231 100644 --- a/label_studio/projects/api.py +++ b/label_studio/projects/api.py @@ -307,7 +307,7 @@ def get_queryset(self): 'total_annotations_number': 10, 'total_predictions_number': 0, 'sampling': 'Sequential sampling', - 'show_ground_truth_first': True, + 'annotator_evaluation_enabled': False, 'show_overlap_first': True, 'overlap_cohort_percentage': 100, 'task_data_login': 'user', diff --git a/label_studio/projects/functions/next_task.md b/label_studio/projects/functions/next_task.md index 15b5154722e4..f5d4b983db4d 100644 --- a/label_studio/projects/functions/next_task.md +++ b/label_studio/projects/functions/next_task.md @@ -21,7 +21,7 @@ flowchart TD B3 -- no --> B4{"LSE low-agreement path?
fflag OPTIC-161
agreement_threshold set
user is annotator"} B4 -- yes --> B6["Filter by agreement threshold
and annotator capacity"] --> B7[Optionally prioritize by low agreement] - B4 -- no --> B8{"Evaluation mode?
fflag ALL-LEAP-1825
show_ground_truth_first"} + B4 -- no --> B8{"Evaluation mode?
fflag ALL-LEAP-1825
annotator_evaluation_enabled"} B8 -- yes --> B7 B8 -- no --> B9[Filter: is_labeled=false] --> B7 end @@ -69,9 +69,7 @@ flowchart TD ### GT-first gating - `should_attempt_ground_truth_first(user, project)` returns true when: - - `show_ground_truth_first=True` and either no `lse_project` or `annotator_evaluation_minimum_tasks` is not set, or - - the user's completed GT-equipped tasks < `annotator_evaluation_minimum_tasks`, or - - minimum tasks reached but the user's GT agreement score is missing or below `annotator_evaluation_minimum_score` (percent). + - `annotator_evaluation_enabled=True` and `annotator_evaluation_onboarding_tasks > 0` and the user's completed GT-equipped tasks < `annotator_evaluation_onboarding_tasks`. - Otherwise returns false (GT-first disabled; proceed via low-agreement/overlap/sampling). ## Queue labels appended to response diff --git a/label_studio/projects/functions/next_task.py b/label_studio/projects/functions/next_task.py index 635b6b598532..6d5163cf3e36 100644 --- a/label_studio/projects/functions/next_task.py +++ b/label_studio/projects/functions/next_task.py @@ -17,14 +17,14 @@ # Hook for GT-first gating (Enterprise can override via settings) -def _oss_should_attempt_gt_first(user: User, project: Project) -> bool: - # Open-source default: if project enables GT-first, allow it without onboarding gates - return bool(project.show_ground_truth_first) +def _lso_should_attempt_gt_first(user: User, project: Project) -> bool: + # Open-source default: if project enables annotator evaluation, allow it without onboarding gates + return bool(project.annotator_evaluation_enabled) get_tasks_agreement_queryset = load_func(settings.GET_TASKS_AGREEMENT_QUERYSET) should_attempt_ground_truth_first = ( - load_func(settings.SHOULD_ATTEMPT_GROUND_TRUTH_FIRST) or _oss_should_attempt_gt_first + load_func(settings.SHOULD_ATTEMPT_GROUND_TRUTH_FIRST) or _lso_should_attempt_gt_first ) @@ -59,10 +59,7 @@ def _get_first_unlocked(tasks_query: QuerySet[Task], user) -> Union[Task, None]: def _try_ground_truth(tasks: QuerySet[Task], project: Project, user: User) -> Union[Task, None]: """Returns task from ground truth set""" - ground_truth = Annotation.objects.filter(task=OuterRef('pk'), ground_truth=True) - not_solved_tasks_with_ground_truths = tasks.annotate(has_ground_truths=Exists(ground_truth)).filter( - has_ground_truths=True - ) + not_solved_tasks_with_ground_truths = _annotate_has_ground_truths(tasks).filter(has_ground_truths=True) if not_solved_tasks_with_ground_truths.exists(): if project.sampling == project.SEQUENCE: return _get_first_unlocked(not_solved_tasks_with_ground_truths, user) @@ -78,13 +75,15 @@ def _try_tasks_with_overlap(tasks: QuerySet[Task]) -> Tuple[Union[Task, None], Q return None, tasks.filter(overlap=1) -def _try_breadth_first(tasks: QuerySet[Task], user: User, project: Project) -> Union[Task, None]: +def _try_breadth_first( + tasks: QuerySet[Task], user: User, project: Project, attempt_gt_first: bool = False +) -> Union[Task, None]: """Try to find tasks with maximum amount of annotations, since we are trying to label tasks as fast as possible""" - # Exclude ground truth annotations from the count when not in onboarding mode + # Exclude ground truth annotations from the count when not in onboarding window # to prevent GT tasks from being prioritized via breadth-first logic annotation_filter = ~Q(annotations__completed_by=user) - if not project.show_ground_truth_first: + if not attempt_gt_first: annotation_filter &= ~Q(annotations__ground_truth=True) tasks = tasks.annotate(annotations_count=Count('annotations', filter=annotation_filter)) @@ -158,13 +157,18 @@ def _try_uncertainty_sampling( return next_task +def _annotate_has_ground_truths(tasks: QuerySet[Task]) -> QuerySet[Task]: + ground_truth = Annotation.objects.filter(task=OuterRef('pk'), ground_truth=True) + return tasks.annotate(has_ground_truths=Exists(ground_truth)) + + def get_not_solved_tasks_qs( user: User, project: Project, prepared_tasks: QuerySet[Task], assigned_flag: Union[bool, None], queue_info: str, - allow_gt_first: bool, + attempt_gt_first: bool, ) -> Tuple[QuerySet[Task], List[int], str, bool]: user_solved_tasks_array = user.annotations.filter(project=project, task__isnull=False) user_solved_tasks_array = user_solved_tasks_array.distinct().values_list('task__pk', flat=True) @@ -188,7 +192,6 @@ def get_not_solved_tasks_qs( and get_tasks_agreement_queryset and user.is_project_annotator(project) ): - # Onboarding mode (GT-first) should keep GT tasks eligible regardless of is_labeled/agreement qs = get_tasks_agreement_queryset(not_solved_tasks) qs = qs.annotate(annotators=Count('annotations__completed_by', distinct=True)) @@ -197,13 +200,10 @@ def get_not_solved_tasks_qs( ) capacity_pred = Q(annotators__lt=F('overlap') + (lse_project.max_additional_annotators_assignable or 0)) - if project.show_ground_truth_first: - gt_subq = Annotation.objects.filter(task=OuterRef('pk'), ground_truth=True) - qs = qs.annotate(has_ground_truths=Exists(gt_subq)) - # Keep all GT tasks + apply low-agreement+capacity to the rest. For sure, we can do: - # - if user.solved_tasks_array.count < lse_project.annotator_evaluation_minimum_tasks - # - else, apply low-agreement+capacity to the rest (maybe performance will be better) - # but it's a question - what is better here. This version is simpler at least from the code perspective. + if project.annotator_evaluation_enabled: + # Include ground truth tasks in the query if annotator evaluation is enabled + qs = _annotate_has_ground_truths(qs) + # Keep all GT tasks + apply low-agreement+capacity to the rest. not_solved_tasks = qs.filter(Q(has_ground_truths=True) | (low_agreement_pred & capacity_pred)) else: not_solved_tasks = qs.filter(low_agreement_pred & capacity_pred) @@ -212,9 +212,15 @@ def get_not_solved_tasks_qs( # otherwise, filtering out completed tasks is sufficient else: - # ignore tasks that are already labeled when GT-first is NOT allowed - if not allow_gt_first: - not_solved_tasks = not_solved_tasks.filter(is_labeled=False) + if not attempt_gt_first: + # Outside of onboarding window + if project.annotator_evaluation_enabled: + # Include ground truth tasks in the query if outside of onboarding window and annotator evaluation is enabled + not_solved_tasks = _annotate_has_ground_truths(not_solved_tasks) + not_solved_tasks = not_solved_tasks.filter(Q(is_labeled=False) | Q(has_ground_truths=True)) + else: + # Ignore tasks that are already labeled when outside of onboarding window and annotator evaluation is not enabled + not_solved_tasks = not_solved_tasks.filter(is_labeled=False) if not flag_set('fflag_fix_back_lsdv_4523_show_overlap_first_order_27022023_short'): # show tasks with overlap > 1 first (unless tasks are already prioritized on agreement) @@ -244,7 +250,7 @@ def get_next_task_without_dm_queue( not_solved_tasks: QuerySet, assigned_flag: Union[bool, None], prioritized_low_agreement: bool, - allow_gt_first: bool, + attempt_gt_first: bool, ) -> Tuple[Union[Task, None], bool, str]: next_task = None use_task_lock = True @@ -265,8 +271,8 @@ def get_next_task_without_dm_queue( use_task_lock = False queue_info += (' & ' if queue_info else '') + 'Task lock' - # Ground truth: use precomputed gating for GT-first - if not next_task and allow_gt_first: + # Ground truth: attempt to label ground truth tasks in onboarding window + if not next_task and attempt_gt_first: logger.debug(f'User={user} tries ground truth from prepared tasks') next_task = _try_ground_truth(not_solved_tasks, project, user) if next_task: @@ -283,7 +289,7 @@ def get_next_task_without_dm_queue( if not next_task and project.maximum_annotations > 1: # if there are already labeled tasks, but task.overlap still < project.maximum_annotations, randomly sampling from them logger.debug(f'User={user} tries depth first from prepared tasks') - next_task = _try_breadth_first(not_solved_tasks, user, project) + next_task = _try_breadth_first(not_solved_tasks, user, project, attempt_gt_first) if next_task: queue_info += (' & ' if queue_info else '') + 'Breadth first queue' @@ -378,16 +384,16 @@ def get_next_task( use_task_lock = True queue_info = '' - # Ground truth: label GT first only during onboarding window for user (gated by min tasks and min score) - allow_gt_first = should_attempt_ground_truth_first(user, project) + # Ground truth: label GT first only during onboarding window for user (gated by onboarding task number) + attempt_gt_first = should_attempt_ground_truth_first(user, project) not_solved_tasks, user_solved_tasks_array, queue_info, prioritized_low_agreement = get_not_solved_tasks_qs( - user, project, prepared_tasks, assigned_flag, queue_info, allow_gt_first + user, project, prepared_tasks, assigned_flag, queue_info, attempt_gt_first ) if not dm_queue: next_task, use_task_lock, queue_info = get_next_task_without_dm_queue( - user, project, not_solved_tasks, assigned_flag, prioritized_low_agreement, allow_gt_first + user, project, not_solved_tasks, assigned_flag, prioritized_low_agreement, attempt_gt_first ) if flag_set('fflag_fix_back_lsdv_4523_show_overlap_first_order_27022023_short'): @@ -452,7 +458,7 @@ def get_next_task( 'maximum_annotations': project.maximum_annotations, 'skip_queue': project.skip_queue, 'sampling': project.sampling, - 'show_ground_truth_first': project.show_ground_truth_first, + 'annotator_evaluation_enabled': project.annotator_evaluation_enabled, 'show_overlap_first': project.show_overlap_first, 'overlap_cohort_percentage': project.overlap_cohort_percentage, 'project_id': project.id, diff --git a/label_studio/projects/migrations/0034_project_annotator_evaluation_enabled.py b/label_studio/projects/migrations/0034_project_annotator_evaluation_enabled.py new file mode 100644 index 000000000000..6159714e9815 --- /dev/null +++ b/label_studio/projects/migrations/0034_project_annotator_evaluation_enabled.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.14 on 2025-12-09 21:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("projects", "0033_projects_soft_delete_indexes_async"), + ] + + operations = [ + migrations.AddField( + model_name="project", + name="annotator_evaluation_enabled", + field=models.BooleanField( + db_default=False, + default=False, + help_text="Enable annotator evaluation for the project", + verbose_name="annotator evaluation enabled", + ), + ), + ] diff --git a/label_studio/projects/models.py b/label_studio/projects/models.py index 5b80d08bc80c..ef908c641ea1 100644 --- a/label_studio/projects/models.py +++ b/label_studio/projects/models.py @@ -315,11 +315,21 @@ class SkipQueue(models.TextChoices): skip_queue = models.CharField( max_length=100, choices=SkipQueue.choices, null=True, default=SkipQueue.REQUEUE_FOR_OTHERS ) + + # Deprecated in favor of annotator_evaluation_enabled show_ground_truth_first = models.BooleanField( _('show ground truth first'), default=False, help_text='Onboarding mode (true): show ground truth tasks first in the labeling stream', ) + + annotator_evaluation_enabled = models.BooleanField( + _('annotator evaluation enabled'), + default=False, + db_default=False, + help_text='Enable annotator evaluation for the project', + ) + show_overlap_first = models.BooleanField(_('show overlap first'), default=False) overlap_cohort_percentage = models.IntegerField(_('overlap_cohort_percentage'), default=100) diff --git a/label_studio/projects/serializers.py b/label_studio/projects/serializers.py index a0da9d1cb150..f0717ef7d9a0 100644 --- a/label_studio/projects/serializers.py +++ b/label_studio/projects/serializers.py @@ -3,6 +3,7 @@ import bleach from constants import SAFE_HTML_ATTRIBUTES, SAFE_HTML_TAGS from django.db.models import Q +from drf_spectacular.utils import extend_schema_serializer from fsm.serializer_fields import FSMStateField from label_studio_sdk.label_interface import LabelInterface from label_studio_sdk.label_interface.control_tags import ( @@ -43,6 +44,7 @@ def __call__(self, serializer_field): return serializer_field.context.get('created_by') +@extend_schema_serializer(deprecate_fields=['show_ground_truth_first']) class ProjectSerializer(FlexFieldsModelSerializer): """Serializer get numbers from project queryset annotation, make sure, that you use correct one(Project.objects.with_counts()) @@ -236,6 +238,7 @@ class Meta: 'total_predictions_number', 'sampling', 'show_ground_truth_first', + 'annotator_evaluation_enabled', 'show_overlap_first', 'overlap_cohort_percentage', 'task_data_login', diff --git a/label_studio/tasks/models.py b/label_studio/tasks/models.py index bc671f661949..dd49d7799255 100644 --- a/label_studio/tasks/models.py +++ b/label_studio/tasks/models.py @@ -289,10 +289,8 @@ def has_lock(self, user=None): """ from projects.functions.next_task import get_next_task_logging_level - if self.project.show_ground_truth_first: - # in show_ground_truth_first mode(onboarding) - # we ignore overlap setting for ground_truth tasks - # https://humansignal.atlassian.net/browse/LEAP-1963 + if self.project.annotator_evaluation_enabled: + # In annotator evaluation mode, ignore overlap setting for ground truth tasks if self.annotations.filter(ground_truth=True).exists(): return False diff --git a/label_studio/tests/data_manager/columns.tavern.yml b/label_studio/tests/data_manager/columns.tavern.yml index 2949382dc911..0f83f1a8cec2 100644 --- a/label_studio/tests/data_manager/columns.tavern.yml +++ b/label_studio/tests/data_manager/columns.tavern.yml @@ -42,7 +42,7 @@ stages: "start_training_on_annotation_update": false, "show_collab_predictions": true, "num_tasks_with_annotations": null, "task_number": null, "useful_annotation_number": null, "ground_truth_number": null, "skipped_annotations_number": null, "total_annotations_number": null, "total_predictions_number": null, "sampling": "Sequential sampling", - "show_ground_truth_first": false, "show_overlap_first": false, "overlap_cohort_percentage": 100, + "show_ground_truth_first": false, "annotator_evaluation_enabled": false, "show_overlap_first": false, "overlap_cohort_percentage": 100, "task_data_login": null, "task_data_password": null, "control_weights": {"label": {"overall": 1.0, "type": "Choices", "labels": {"pos": 1.0, "neg": 1.0}}}, "parsed_label_config": { diff --git a/poetry.lock b/poetry.lock index 7f4e350e3d6f..4ba10021b07d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "annotated-types" @@ -2131,7 +2131,7 @@ optional = false python-versions = ">=3.9,<4" groups = ["main"] files = [ - {file = "58970c14d2c1683e5093eae848a8265cbbc4acac.zip", hash = "sha256:de26d6a1e4e2a704568482e87078e68e201af48574eca18be07d5c626889d444"}, + {file = "8ce1b4f80f12780da5d07184e4bea25a5c05fe96.zip", hash = "sha256:30336a2d65c5ae4c2d9d7fdc9c22e24b1d285155c93c56ad47a7c34ebc4bfef7"}, ] [package.dependencies] @@ -2159,7 +2159,7 @@ xmljson = "0.2.1" [package.source] type = "url" -url = "https://github.com/HumanSignal/label-studio-sdk/archive/58970c14d2c1683e5093eae848a8265cbbc4acac.zip" +url = "https://github.com/HumanSignal/label-studio-sdk/archive/8ce1b4f80f12780da5d07184e4bea25a5c05fe96.zip" [[package]] name = "launchdarkly-server-sdk" @@ -5110,4 +5110,4 @@ uwsgi = ["pyuwsgi", "uwsgitop"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4" -content-hash = "57694e7f849581974361ddc59b625bd947f182605f6227137614bd708fa304a5" +content-hash = "8f10d0178c0fe4f6a56ed63e0be4ab1635f8bde65cab0ecc4a2442c960c8e27d" diff --git a/pyproject.toml b/pyproject.toml index 4d89176f29cb..ca6bf9033cac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -74,7 +74,7 @@ dependencies = [ "tldextract (>=5.1.3)", "uuid-utils (>=0.11.0,<1.0.0)", ## HumanSignal repo dependencies :start - "label-studio-sdk @ https://github.com/HumanSignal/label-studio-sdk/archive/58970c14d2c1683e5093eae848a8265cbbc4acac.zip", + "label-studio-sdk @ https://github.com/HumanSignal/label-studio-sdk/archive/8ce1b4f80f12780da5d07184e4bea25a5c05fe96.zip", ## HumanSignal repo dependencies :end ] diff --git a/web/apps/labelstudio/src/config/ApiConfig.example.js b/web/apps/labelstudio/src/config/ApiConfig.example.js index 26fcd3facf00..b9de7c1f9ba7 100644 --- a/web/apps/labelstudio/src/config/ApiConfig.example.js +++ b/web/apps/labelstudio/src/config/ApiConfig.example.js @@ -51,7 +51,7 @@ export const API_CONFIG = { total_annotations_number: 300, total_predictions_number: 100, sampling: "Sequential sampling", - show_ground_truth_first: false, + annotator_evaluation_enabled: false, show_overlap_first: false, overlap_cohort_percentage: 100, task_data_login: null, diff --git a/web/apps/labelstudio/src/types/Project.d.ts b/web/apps/labelstudio/src/types/Project.d.ts index 733a00682afc..c2363ad5b5e9 100644 --- a/web/apps/labelstudio/src/types/Project.d.ts +++ b/web/apps/labelstudio/src/types/Project.d.ts @@ -72,7 +72,7 @@ declare type APIProject = { /** Total predictions number in project including skipped_annotations_number and ground_truth_number. */ total_predictions_number?: string; sampling?: "Sequential sampling" | "Uniform sampling" | "Uncertainty sampling" | null; - show_ground_truth_first?: boolean; + annotator_evaluation_enabled?: boolean; show_overlap_first?: boolean; overlap_cohort_percentage?: number; diff --git a/web/libs/ui/src/assets/icons/index.ts b/web/libs/ui/src/assets/icons/index.ts index 16833cbedd18..ac81bd026b78 100644 --- a/web/libs/ui/src/assets/icons/index.ts +++ b/web/libs/ui/src/assets/icons/index.ts @@ -178,6 +178,10 @@ export { ReactComponent as IconMinus } from "./minus.svg"; export { ReactComponent as IconModel } from "./model.svg"; export { ReactComponent as IconModels } from "./models.svg"; export { ReactComponent as IconModelVersion } from "./model-version.svg"; +export { ReactComponent as IconMoveLeft } from "./move-left.svg"; +export { ReactComponent as IconMoveRight } from "./move-right.svg"; +export { ReactComponent as IconMoveUp } from "./move-up.svg"; +export { ReactComponent as IconMoveDown } from "./move-down.svg"; export { ReactComponent as IconMoveTool } from "./move-tool.svg"; export { ReactComponent as IconNext } from "./next-step.svg"; export { ReactComponent as IconOctagonAlert } from "./octagon-alert.svg"; diff --git a/web/libs/ui/src/assets/icons/move-down.svg b/web/libs/ui/src/assets/icons/move-down.svg new file mode 100644 index 000000000000..0c676f0e31a0 --- /dev/null +++ b/web/libs/ui/src/assets/icons/move-down.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/libs/ui/src/assets/icons/move-left.svg b/web/libs/ui/src/assets/icons/move-left.svg new file mode 100644 index 000000000000..621e893da44c --- /dev/null +++ b/web/libs/ui/src/assets/icons/move-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/libs/ui/src/assets/icons/move-right.svg b/web/libs/ui/src/assets/icons/move-right.svg new file mode 100644 index 000000000000..3fb180ecedd3 --- /dev/null +++ b/web/libs/ui/src/assets/icons/move-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/libs/ui/src/assets/icons/move-up.svg b/web/libs/ui/src/assets/icons/move-up.svg new file mode 100644 index 000000000000..d131df2540af --- /dev/null +++ b/web/libs/ui/src/assets/icons/move-up.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/web/libs/ui/src/shad/components/ui/badge.tsx b/web/libs/ui/src/shad/components/ui/badge.tsx index 7a30a94bf538..073ad155c507 100644 --- a/web/libs/ui/src/shad/components/ui/badge.tsx +++ b/web/libs/ui/src/shad/components/ui/badge.tsx @@ -12,6 +12,8 @@ const badgeVariants = cva( secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", success: "border-transparent bg-positive-background text-positive-content hover:bg-positive-background/80", + warning: + "bg-warning-background border-warning-border-subtlest text-warning-content hover:bg-warning-background/80", info: "bg-primary-background border-primary-emphasis text-accent-grape-dark font-normal", outline: "text-neutral-content border-neutral-border", beta: "bg-accent-plum-subtle text-accent-plum-dark font-medium border-transparent",