Bin
2025-12-17 d616898802dfe7e5dd648bcf53c6d1f86b6d3642
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import { useMemo } from "react";
import { observer } from "mobx-react";
import { useEffect, useState } from "react";
import { Button, IconChevronLeft, IconChevronRight, Tooltip } from "@humansignal/ui";
import { cn } from "../../utils/bem";
import { FF_DEV_3873, FF_DEV_4174, FF_LEAP_1173, FF_TASK_COUNT_FIX, isFF } from "../../utils/feature-flags";
import { guidGenerator } from "../../utils/unique";
import { isDefined } from "../../utils/utilities";
import "./CurrentTask.scss";
import { reaction } from "mobx";
 
export const CurrentTask = observer(({ store }) => {
  const currentIndex = useMemo(() => {
    return store.taskHistory.findIndex((x) => x.taskId === store.task.id) + 1;
  }, [store.taskHistory]);
 
  const [initialCommentLength, setInitialCommentLength] = useState(0);
  const [visibleComments, setVisibleComments] = useState(0);
 
  useEffect(() => {
    store.commentStore.setAddedCommentThisSession(false);
    const reactionDisposer = reaction(
      () => store.commentStore.comments.map((item) => item.isDeleted),
      (result) => {
        setVisibleComments(result.filter((item) => !item).length);
      },
    );
    return () => {
      reactionDisposer?.();
    };
  }, []);
 
  useEffect(() => {
    if (store.commentStore.addedCommentThisSession) {
      setInitialCommentLength(visibleComments);
    }
  }, [store.commentStore.addedCommentThisSession]);
 
  // Manager roles that can force-skip unskippable tasks (OW=Owner, AD=Admin, MA=Manager)
  const MANAGER_ROLES = ["OW", "AD", "MA"];
 
  const historyEnabled = store.hasInterface("topbar:prevnext");
  const showCounter = store.hasInterface("topbar:task-counter");
  const task = store.task;
  const taskAllowSkip = task?.allow_skip !== false;
  const userRole = window.APP_SETTINGS?.user?.role;
  const hasForceSkipPermission = MANAGER_ROLES.includes(userRole);
  const canSkipOrPostpone = taskAllowSkip || hasForceSkipPermission;
 
  // Check if user has submitted an annotation (pk is defined means annotation is in database)
  const hasSubmittedAnnotation = isDefined(store.annotationStore.selected.pk);
 
  // If task cannot be skipped and user doesn't have force_skip, also disable postpone
  // Note: store.hasInterface("postpone") is set by lsf-sdk based on task.allow_postpone from API
  let canPostpone =
    !hasSubmittedAnnotation &&
    (!isFF(FF_LEAP_1173) || store.hasInterface("skip")) &&
    !store.canGoNextTask &&
    !store.hasInterface("review") &&
    store.hasInterface("postpone") &&
    canSkipOrPostpone;
 
  if (store.hasInterface("annotations:comments") && isFF(FF_DEV_4174)) {
    canPostpone = canPostpone && store.commentStore.addedCommentThisSession && visibleComments >= initialCommentLength;
  }
 
  // For unskippable tasks, force user to submit annotation before navigating
  // Block both history navigation (next task) and postpone if no annotation submitted
  const requiresAnnotationSubmission = !taskAllowSkip && !hasForceSkipPermission && !hasSubmittedAnnotation;
  const canNavigateNext = store.canGoNextTask && !requiresAnnotationSubmission;
  const canPostponeTask = canPostpone && !requiresAnnotationSubmission;
 
  // Memoized messages for previous button
  const prevButtonMessage = useMemo(() => {
    return !store.canGoPrevTask ? "没有上一任务" : "上一任务";
  }, [store.canGoPrevTask]);
 
  // Memoized messages for next button
  const nextButtonMessage = useMemo(() => {
    if (requiresAnnotationSubmission) {
      return "提交标注以继续";
    }
    if (canNavigateNext) {
      return "下一任务";
    }
    if (canPostponeTask) {
      return "推迟任务";
    }
    if (!canSkipOrPostpone) {
      return "无法推迟:任务不可跳过";
    }
    return "没有下一任务";
  }, [requiresAnnotationSubmission, canNavigateNext, canPostponeTask, canSkipOrPostpone]);
 
  return (
    <div className={cn("topbar").elem("section").toClassName()}>
      <div
        className={cn("current-task").mod({ "with-history": historyEnabled }).toClassName()}
        style={{
          padding: isFF(FF_DEV_3873) && 0,
          width: isFF(FF_DEV_3873) && "auto",
        }}
      >
        <div
          className={cn("current-task").elem("task-id").toClassName()}
          style={{ fontSize: isFF(FF_DEV_3873) ? 12 : 14 }}
        >
          {store.task.id ?? guidGenerator()}
          {historyEnabled &&
            showCounter &&
            (isFF(FF_TASK_COUNT_FIX) ? (
              <div className={cn("current-task").elem("task-count").toClassName()}>
                {store.queuePosition} / {store.queueTotal}
              </div>
            ) : (
              <div className={cn("current-task").elem("task-count").toClassName()}>
                {currentIndex} / {store.taskHistory.length}
              </div>
            ))}
        </div>
        {historyEnabled && (
          <div
            className={cn("current-task")
              .elem("history-controls")
              .mod({ newui: isFF(FF_DEV_3873) })
              .toClassName()}
          >
            <Tooltip title={prevButtonMessage} alignment="bottom-center">
              <Button
                data-testid="prev-task"
                aria-label={prevButtonMessage}
                look="string"
                disabled={!historyEnabled || !store.canGoPrevTask}
                onClick={store.prevTask}
                style={{ background: !isFF(FF_DEV_3873) && "none", backgroundColor: isFF(FF_DEV_3873) && "none" }}
                variant="neutral"
              >
                <IconChevronLeft />
              </Button>
            </Tooltip>
            <Tooltip title={nextButtonMessage} alignment="bottom-center">
              <Button
                data-testid="next-task"
                aria-label={nextButtonMessage}
                look="string"
                disabled={!canNavigateNext && !canPostponeTask}
                onClick={canNavigateNext ? store.nextTask : store.postponeTask}
                style={{ background: !isFF(FF_DEV_3873) && "none", backgroundColor: isFF(FF_DEV_3873) && "none" }}
                variant={!canNavigateNext && canPostponeTask ? "primary" : "neutral"}
              >
                <IconChevronRight />
              </Button>
            </Tooltip>
          </div>
        )}
      </div>
    </div>
  );
});