Bin
2025-12-16 9e0b2ba2c317b1a86212f24cbae3195ad1f3dbfa
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
/**
 * State Registry System for Label Studio
 *
 * This module provides an extensible state management system that allows
 * Label Studio Enterprise to extend state definitions without modifying base code.
 *
 * Instead of mapping states directly to colors (CREATED → "grey"), we use semantic
 * types that represent meaning (CREATED → StateType.INITIAL → neutral styling).
 *
 * This allows:
 * - Clear intent when reading code (TERMINAL vs green)
 * - Easy visual redesigns without touching logic
 * - Consistent styling across similar state types
 * - Entity-specific tooltips (same state, different descriptions)
 * - LSE extension without modifying LSO code
 */
 
/**
 * Semantic state categories that define the meaning and visual representation of states.
 *
 * States are not just colors - they represent different phases in a workflow:
 * - INITIAL: Starting point, newly created entities
 * - IN_PROGRESS: Active work happening
 * - ATTENTION: Requires intervention or review
 * - TERMINAL: Completed, no further changes expected
 */
export enum StateType {
  INITIAL = "initial",
  IN_PROGRESS = "in_progress",
  ATTENTION = "attention",
  TERMINAL = "terminal",
}
 
/**
 * Entity types that can have states.
 * Used for entity-specific tooltip lookup.
 */
export type EntityType = "task" | "annotation" | "project" | "annotationreview";
 
/**
 * State metadata including type, label, and entity-specific tooltips.
 */
export interface StateMetadata {
  /** Semantic state type determining visual styling */
  type: StateType;
 
  /** Human-readable label for display (defaults to formatted state name if not provided) */
  label?: string;
 
  /** Entity-specific tooltip descriptions */
  tooltips?: Partial<Record<EntityType, string>>;
}
 
/**
 * Tailwind CSS classes for each state type.
 * Using semantic design tokens for maintainable theming.
 */
const STATE_TYPE_STYLES: Record<StateType, string> = {
  [StateType.INITIAL]: "bg-neutral-emphasis border-neutral-border text-neutral-content",
  [StateType.IN_PROGRESS]: "bg-primary-emphasis border-primary-border-subtlest text-primary-content",
  [StateType.ATTENTION]: "bg-warning-emphasis border-warning-border-subtlest text-warning-content",
  [StateType.TERMINAL]: "bg-positive-emphasis border-positive-border-subtlest text-positive-content",
};
 
/**
 * Central registry for state definitions.
 *
 * This singleton class provides:
 * - Registration of state metadata
 * - Lookup of state types and tooltips
 * - Extension mechanism for LSE
 */
class StateRegistry {
  private states = new Map<string, StateMetadata>();
 
  /**
   * Register a state with its metadata.
   * Can be called multiple times for the same state to update metadata.
   *
   * @param state - State constant (e.g., 'CREATED', 'IN_PROGRESS')
   * @param metadata - State type, label, and tooltips
   */
  register(state: string, metadata: StateMetadata): void {
    this.states.set(state, metadata);
  }
 
  /**
   * Register multiple states at once.
   * Useful for batch registration of related states.
   *
   * @param states - Map of state constants to metadata
   */
  registerBatch(states: Record<string, StateMetadata>): void {
    Object.entries(states).forEach(([state, metadata]) => {
      this.register(state, metadata);
    });
  }
 
  /**
   * Get the semantic type of a state.
   * Falls back to INITIAL if state is not registered.
   *
   * @param state - State constant
   * @returns StateType enum value
   */
  getType(state: string): StateType {
    return this.states.get(state)?.type ?? StateType.INITIAL;
  }
 
  /**
   * Get the display label for a state.
   * Falls back to formatted state name if no label is registered.
   *
   * @param state - State constant
   * @returns Human-readable label
   */
  getLabel(state: string): string {
    const metadata = this.states.get(state);
    return metadata?.label ?? this.formatStateName(state);
  }
 
  /**
   * Get the tooltip description for a state + entity combination.
   * Falls back to generic description if no entity-specific tooltip exists.
   *
   * @param state - State constant
   * @param entityType - Type of entity (task, project, etc.)
   * @returns Tooltip text
   */
  getTooltip(state: string, entityType: EntityType): string {
    const metadata = this.states.get(state);
 
    if (!metadata?.tooltips) {
      // No tooltips defined, return generic description
      return `${this.getLabel(state)} state`;
    }
 
    // Look up entity-specific tooltip, fall back to first available tooltip
    const entityTooltip = metadata.tooltips[entityType];
    if (entityTooltip) {
      return entityTooltip;
    }
 
    // Fall back to any available tooltip
    const firstTooltip = Object.values(metadata.tooltips)[0];
    return firstTooltip ?? `${this.getLabel(state)} state`;
  }
 
  /**
   * Get Tailwind CSS classes for a state's visual styling.
   *
   * @param state - State constant
   * @returns Space-separated Tailwind class names
   */
  getStyleClasses(state: string): string {
    const stateType = this.getType(state);
    return STATE_TYPE_STYLES[stateType];
  }
 
  /**
   * Check if a state is registered.
   *
   * @param state - State constant
   * @returns true if state is registered
   */
  isRegistered(state: string): boolean {
    return this.states.has(state);
  }
 
  /**
   * Get all registered states.
   * Useful for debugging and testing.
   *
   * @returns Array of state constants
   */
  getAllStates(): string[] {
    return Array.from(this.states.keys());
  }
 
  /**
   * Get states that apply to a specific entity type.
   * A state applies to an entity if it has a tooltip defined for that entity type.
   *
   * @param entityType - Type of entity (task, annotation, project, annotationreview)
   * @returns Array of state constants applicable to the entity type
   */
  getStatesByEntityType(entityType: EntityType): string[] {
    return Array.from(this.states.entries())
      .filter(([_, metadata]) => metadata.tooltips && entityType in metadata.tooltips)
      .sort((a, b) => {
        // Sort by logical workflow order: INITIAL → IN_PROGRESS → ATTENTION → TERMINAL
        const workflowOrder = [StateType.INITIAL, StateType.IN_PROGRESS, StateType.ATTENTION, StateType.TERMINAL];
        const aType = this.getType(a[0]);
        const bType = this.getType(b[0]);
        const typeDiff = workflowOrder.indexOf(aType) - workflowOrder.indexOf(bType);
        if (typeDiff !== 0) return typeDiff;
 
        // Within each type, sort by label alphabetically
        // BUT: Special cases for specific states
        const aState = a[0];
        const bState = b[0];
 
        // Special case: IN_PROGRESS state should be first in its type group
        if (aState === "IN_PROGRESS") return -1;
        if (bState === "IN_PROGRESS") return 1;
 
        // Special case: COMPLETED (Done) should always be last
        if (aState === "COMPLETED") return 1;
        if (bState === "COMPLETED") return -1;
 
        // Otherwise sort by label alphabetically
        const aLabel = this.getLabel(aState);
        const bLabel = this.getLabel(bState);
        return aLabel.localeCompare(bLabel);
      })
      .map(([state]) => state);
  }
 
  /**
   * Format a state constant into a human-readable name.
   * Converts SNAKE_CASE to Title Case.
   *
   * @param state - State constant (e.g., 'ANNOTATION_IN_PROGRESS')
   * @returns Formatted name (e.g., 'Annotation In Progress')
   */
  private formatStateName(state: string): string {
    return state
      .split("_")
      .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
      .join(" ");
  }
}
 
/**
 * Singleton instance of the state registry.
 * Import this to register or query states.
 */
export const stateRegistry = new StateRegistry();
 
// ============================================================================
// Core State Registrations (Label Studio Open Source)
// ============================================================================
 
/**
 * Minimal LSO states - just the basics.
 * All review, arbitration, and advanced workflow states are in LSE.
 */
stateRegistry.registerBatch({
  CREATED: {
    type: StateType.INITIAL,
    label: "Initial",
    tooltips: {
      task: "Task has been created and is ready for annotation",
      annotation: "Annotation has been created",
      project: "Project has been created and is ready for configuration",
    },
  },
 
  ANNOTATION_IN_PROGRESS: {
    type: StateType.IN_PROGRESS,
    label: "Annotating",
    tooltips: {
      task: "Task is currently being annotated",
      project: "Annotation work is in progress on this project",
    },
  },
 
  COMPLETED: {
    type: StateType.TERMINAL,
    label: "Done",
    tooltips: {
      task: "Task is fully completed and no further work is needed",
      annotation: "Annotation is completed and finalized",
      project: "Project is completed - all tasks are done",
    },
  },
});
 
// ============================================================================
// Helper Functions
// ============================================================================
 
/**
 * Get Tailwind CSS classes for a state's visual styling.
 *
 * @param state - State constant (e.g., 'CREATED', 'IN_PROGRESS')
 * @returns Space-separated Tailwind class names
 */
export function getStateColorClass(state: string): string {
  return stateRegistry.getStyleClasses(state);
}
 
/**
 * Format a state constant into a human-readable name.
 *
 * @param state - State constant (e.g., 'ANNOTATION_IN_PROGRESS')
 * @returns Formatted name (e.g., 'Annotating')
 */
export function formatStateName(state: string): string {
  return stateRegistry.getLabel(state);
}
 
/**
 * Get the tooltip description for a state + entity combination.
 *
 * @param state - State constant
 * @param entityType - Type of entity (task, annotation, project, annotationreview)
 * @returns Tooltip description text
 */
export function getStateDescription(state: string, entityType: EntityType = "task"): string {
  return stateRegistry.getTooltip(state, entityType);
}
 
/**
 * Get the semantic type of a state.
 * Useful for conditional logic based on state category.
 *
 * @param state - State constant
 * @returns StateType enum value
 */
export function getStateType(state: string): StateType {
  return stateRegistry.getType(state);
}
 
/**
 * Check if a state represents a terminal (completed) state.
 *
 * @param state - State constant
 * @returns true if state is terminal
 */
export function isTerminalState(state: string): boolean {
  return stateRegistry.getType(state) === StateType.TERMINAL;
}
 
/**
 * Check if a state requires attention/intervention.
 *
 * @param state - State constant
 * @returns true if state requires attention
 */
export function requiresAttention(state: string): boolean {
  return stateRegistry.getType(state) === StateType.ATTENTION;
}