Annotation Form Components#
The annotation form is Langfuse's manual scoring UI, allowing users to assign numeric, categorical, or free-text scores to traces and observations. The core implementation lives in web/src/features/scores/components/.
Entry Point: AnnotationForm and AnnotateDrawer#
AnnotationForm is the public-facing component. It:
- Loads available score configs via
useAnnotationScoreConfigs(supports"fixed"and"selectable"modes). - Merges server scores with the optimistic cache via
useMergedAnnotationScores. - Builds
initialFormDataby combining existing scores with any selected-but-unscored configs . - Renders a skeleton on load, then delegates to
InnerAnnotationForm.
AnnotateDrawer wraps AnnotationForm in a sheet/drawer and provides the "Annotate" button trigger. It is used in trace and observation detail headers, preview panels, and session views .
Inner Form: InnerAnnotationForm#
InnerAnnotationForm owns all interactive form logic. It uses react-hook-form with a useFieldArray over scoreData, validated by AnnotateFormSchema (a Zod schema wrapping an array of AnnotationScoreDataSchema).
Per-score input rendering switches on dataType :
| Data type | Input component | Trigger |
|---|---|---|
TEXT | <Textarea> | onBlur → handleTextUpsert |
NUMERIC | <Input type="number"> | onBlur → handleNumericUpsert |
| Categorical (≤3 short labels) | <ToggleGroup> | onValueChange → handleCategoricalUpsert |
| Categorical (>3 or long labels) | <Combobox> | onValueChange → handleCategoricalUpsert |
The switch between ToggleGroup and Combobox for categorical scores uses renderSelect: Combobox is used when there are more than 3 categories, or more than 1 category with labels longer than 6 characters.
Numeric Validation#
validateNumericScore in annotationFormHelpers.ts checks that a numeric input falls within the [minValue, maxValue] range defined by the score config. If out of range, it returns an error string like "Not in range: [0,1]". This validation runs inside handleNumericUpsert on blur — if the check fails, the error is set via form.setError(...) and the mutation is suppressed.
Form State & Optimistic Updates#
All upserts go through handleUpsert, which:
- Clears errors and writes values to the form optimistically.
- Calls
createMutation(new score) orupdateMutation(existing score) fromuseScoreMutations. - Rolls back form state on mutation error via
form.setValue+form.setErrorwith a"server"error type.
Delete operations follow the same pattern in handleDeleteScore: fields are cleared optimistically, with rollback on error.
A showSaving boolean tracks pending mutation state and drives the "Saving score data" / "Score data saved" indicator in AnnotateHeader .
Stale Category Handling#
When a score config's categories change after a score has been saved, enrichCategoryOptionsWithStaleScoreValue prepends the old label as an isOutdated: true entry. In the UI, outdated options are rendered disabled in both ToggleGroup and Combobox .
Key Supporting Files#
| File | Purpose |
|---|---|
annotationFormHelpers.ts | validateNumericScore, enrichCategoryOptionsWithStaleScoreValue, resolveConfigValue |
schema.ts | AnnotateFormSchema — Zod schema for form validation |
types.ts | AnnotationScoreFormData, InnerAnnotationFormProps, ScoreTarget, AnnotationForm prop types |
helpers.ts | isNumericDataType, isTextDataType, isScoreUnsaved, formatAnnotateDescription |
useScoreMutations.ts | Create/update/delete mutations with optimistic cache and toast error handling |
useMergedAnnotationScores.ts | Merges server scores with optimistic cache for initial form state |
useScoreConfigs.ts | useAnnotationScoreConfigs — loads configs in fixed or selectable mode |
useScoreConfigSelection.ts | Manages dynamic config selection options in selectable mode |
multi-select-key-values.tsx | Multi-select dropdown for adding/removing score configs in selectable mode |
Score Comment Flow#
Comments are handled by the local CommentField component inside a <Popover>. It tracks localValue in state, diffs against the saved comment, and exposes "Save Changes" / "Discard Changes" buttons. On save, it calls handleCommentUpdate, which fires updateMutation with rollback.