import { useCallback, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useShallow } from "zustand/react/shallow";
import { Flex, Label, Switch, Text } from "@phoenix/components";
import { BuiltInEvaluatorOutputConfig } from "@phoenix/components/evaluators/BuiltInEvaluatorOutputConfig";
import { ContainsEvaluatorCodeBlock } from "@phoenix/components/evaluators/ContainsEvaluatorCodeBlock";
import { useFlattenedEvaluatorInputKeys } from "@phoenix/components/evaluators/EvaluatorInputMapping";
import { SwitchableEvaluatorInput } from "@phoenix/components/evaluators/SwitchableEvaluatorInput";
import {
useEvaluatorStore,
useEvaluatorStoreInstance,
} from "@phoenix/contexts/EvaluatorContext/useEvaluatorStore";
const useContainsEvaluatorForm = () => {
const store = useEvaluatorStoreInstance();
const { pathMapping, literalMapping } = useEvaluatorStore(
(state) => state.evaluator.inputMapping
);
const form = useForm({
defaultValues: { pathMapping, literalMapping },
mode: "onChange",
});
const subscribe = form.subscribe;
useEffect(() => {
return subscribe({
formState: { isValid: true, values: true },
callback({ values: { pathMapping, literalMapping }, isValid }) {
if (!isValid) {
return;
}
const { setPathMapping, setLiteralMapping } = store.getState();
setPathMapping({ ...pathMapping });
setLiteralMapping({ ...literalMapping });
},
});
}, [subscribe, store]);
return form;
};
export const ContainsEvaluatorForm = () => {
const { control, getValues, setValue, trigger } = useContainsEvaluatorForm();
const store = useEvaluatorStoreInstance();
const [containsTextPath, setContainsTextPath] = useState<string>(
() => getValues("pathMapping.text") ?? ""
);
const { evaluatorMappingSource } = useEvaluatorStore(
useShallow((state) => ({
evaluatorMappingSource: state.evaluatorMappingSource,
}))
);
const allExampleKeys = useFlattenedEvaluatorInputKeys(evaluatorMappingSource);
// Register validator for required SwitchableEvaluatorInput fields.
// Triggering both path and literal names is safe because only the
// mounted Controller is registered with RHF at any time.
const triggerValidation = useCallback(async () => {
return trigger([
"pathMapping.text",
"literalMapping.text",
"pathMapping.words",
"literalMapping.words",
]);
}, [trigger]);
useEffect(() => {
const unregister = store
.getState()
.registerValidator("containsFields", triggerValidation);
return unregister;
}, [store, triggerValidation]);
// Determine initial mode based on existing values
const textDefaultMode =
getValues("literalMapping.text") != null ? "literal" : "path";
const wordsDefaultMode =
getValues("pathMapping.words") != null ? "path" : "literal";
return (
<Flex direction="column" gap="size-200">
<Flex direction="column" gap="size-100">
<SwitchableEvaluatorInput
fieldName="text"
label="Text"
description="The text to search for the words in."
defaultMode={textDefaultMode}
control={control}
setValue={setValue}
pathOptions={allExampleKeys}
pathPlaceholder="Map an example field to Text"
literalPlaceholder="Enter literal text value"
pathInputValue={containsTextPath}
onPathInputChange={setContainsTextPath}
isRequired
/>
<SwitchableEvaluatorInput
fieldName="words"
label="Words"
description="A comma separated list of words to search for in the text."
defaultMode={wordsDefaultMode}
control={control}
setValue={setValue}
pathOptions={allExampleKeys}
pathPlaceholder="Map an example field to Words"
literalPlaceholder="e.g. word1, word2, word3"
isRequired
/>
<Controller
name="literalMapping.case_sensitive"
control={control}
defaultValue={false}
render={({ field }) => (
<Switch
{...field}
value={String(field.value ?? "")}
onChange={(value) => field.onChange(value)}
isSelected={Boolean(
typeof field.value === "boolean"
? field.value
: typeof field.value === "string"
? field.value.toLowerCase() === "true"
: false
)}
>
<Label>Case sensitive</Label>
<Text slot="description">
Whether to match the words case sensitive.
</Text>
</Switch>
)}
/>
<Controller
name="literalMapping.require_all"
control={control}
defaultValue={false}
render={({ field }) => (
<Switch
{...field}
value={String(field.value ?? "")}
onChange={(value) => field.onChange(value)}
isSelected={Boolean(
typeof field.value === "boolean"
? field.value
: typeof field.value === "string"
? field.value.toLowerCase() === "true"
: false
)}
>
<Label>Require all</Label>
<Text slot="description">
Whether to require all words in the list to be present in the
text. If false, any word in the list can be present in the text.
</Text>
</Switch>
)}
/>
</Flex>
<BuiltInEvaluatorOutputConfig />
<ContainsEvaluatorCodeBlock />
</Flex>
);
};