Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,17 @@ const actionCenterApi = baseApi.injectEndpoints({
}),
invalidatesTags: ["Monitor Field Results", "Monitor Field Details"],
}),
classifyWebsiteAssets: build.mutation<
MonitorActionResponse,
{ monitor_config_key: string; staged_resource_urns?: string[] }
>({
query: ({ monitor_config_key, staged_resource_urns }) => ({
url: `/plus/discovery-monitor/${monitor_config_key}/classify-assets`,
method: "POST",
body: staged_resource_urns ? { staged_resource_urns } : {},
}),
invalidatesTags: ["Discovery Monitor Results"],
}),
}),
});

Expand Down Expand Up @@ -591,4 +602,5 @@ export const {
useGetStagedResourceDetailsQuery,
useLazyGetStagedResourceDetailsQuery,
usePromoteRemovalStagedResourcesMutation,
useClassifyWebsiteAssetsMutation,
} = actionCenterApi;
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import {
useAddMonitorResultAssetsMutation,
useAddMonitorResultSystemsMutation,
useClassifyWebsiteAssetsMutation,
useGetDiscoveredAssetsQuery,
useGetWebsiteMonitorResourceFiltersQuery,
useIgnoreMonitorResultAssetsMutation,
Expand Down Expand Up @@ -139,14 +140,17 @@ export const useDiscoveredAssetsTable = ({
restoreMonitorResultAssetsMutation,
{ isLoading: isRestoringResults },
] = useRestoreMonitorResultAssetsMutation();
const [classifyWebsiteAssetsMutation, { isLoading: isClassifyingAssets }] =
useClassifyWebsiteAssetsMutation();

const anyBulkActionIsLoading =
isAddingResults ||
isIgnoringResults ||
isAddingAllResults ||
isBulkUpdatingSystem ||
isRestoringResults ||
isBulkAddingDataUses;
isBulkAddingDataUses ||
isClassifyingAssets;

const disableAddAll =
anyBulkActionIsLoading || systemId === UNCATEGORIZED_SEGMENT;
Expand Down Expand Up @@ -612,6 +616,31 @@ export const useDiscoveredAssetsTable = ({
resetSelections,
]);

const handleClassifyWithAI = useCallback(async () => {
const result = await classifyWebsiteAssetsMutation({
monitor_config_key: monitorId,
staged_resource_urns: selectedUrns.length > 0 ? selectedUrns : undefined,
});
if (isErrorResult(result)) {
toast(errorToastParams(getErrorMessage(result.error)));
} else {
const count = selectedUrns.length > 0 ? selectedUrns.length : "all";
toast(
successToastParams(
`Classification complete for ${count} uncategorized assets.`,
`Confirmed`,
),
);
resetSelections();
}
}, [
classifyWebsiteAssetsMutation,
monitorId,
selectedUrns,
toast,
resetSelections,
]);

const handleAddAll = useCallback(async () => {
const assetCount = data?.items.length || 0;
const result = await addMonitorResultSystemsMutation({
Expand Down Expand Up @@ -699,6 +728,7 @@ export const useDiscoveredAssetsTable = ({
handleBulkIgnore,
handleBulkRestore,
handleAddAll,
handleClassifyWithAI,

// Loading states
anyBulkActionIsLoading,
Expand All @@ -708,6 +738,7 @@ export const useDiscoveredAssetsTable = ({
isBulkUpdatingSystem,
isBulkAddingDataUses,
isRestoringResults,
isClassifyingAssets,
disableAddAll,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from "fidesui";
import { useState } from "react";

import { useFlags } from "~/features/common/features/features.slice";
import { SelectedText } from "~/features/common/table/SelectedText";
import {
ConsentAlertInfo,
Expand All @@ -37,6 +38,10 @@ export const DiscoveredAssetsTable = ({
systemId,
consentStatus,
}: DiscoveredAssetsTableProps) => {
// Feature flags
const { flags } = useFlags();
const showLlmClassification = flags.alphaWebMonitorLlmClassification;

// Modal state
const [isAssignSystemModalOpen, setIsAssignSystemModalOpen] =
useState<boolean>(false);
Expand Down Expand Up @@ -83,12 +88,14 @@ export const DiscoveredAssetsTable = ({
handleBulkIgnore,
handleBulkRestore,
handleAddAll,
handleClassifyWithAI,

// Loading states
anyBulkActionIsLoading,
isAddingAllResults,
isBulkUpdatingSystem,
isBulkAddingDataUses,
isClassifyingAssets,
disableAddAll,
} = useDiscoveredAssetsTable({
monitorId,
Expand Down Expand Up @@ -186,6 +193,18 @@ export const DiscoveredAssetsTable = ({
label: "Ignore",
onClick: handleBulkIgnore,
},
...(showLlmClassification
? [
{
type: "divider" as const,
},
{
key: "classify-ai",
label: "Classify with AI",
onClick: handleClassifyWithAI,
},
]
: []),
]),
],
}}
Expand All @@ -205,6 +224,16 @@ export const DiscoveredAssetsTable = ({
</Button>
</Dropdown>

{showLlmClassification && (
<Button
onClick={handleClassifyWithAI}
loading={isClassifyingAssets}
disabled={anyBulkActionIsLoading || actionsDisabled}
data-testid="classify-ai"
>
Classify with AI
</Button>
)}
<Tooltip
title={
disableAddAll
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { format, parseISO } from "date-fns";
import dayjs from "dayjs";
import {
AntTooltip as Tooltip,
Button,
ChakraText as Text,
Flex,
Expand All @@ -12,13 +13,15 @@ import { useRouter } from "next/router";
import { useState } from "react";
import * as Yup from "yup";

import { useFlags } from "~/features/common/features/features.slice";
import { FormikDateTimeInput } from "~/features/common/form/FormikDateTimeInput";
import { FormikLocationSelect } from "~/features/common/form/FormikLocationSelect";
import { FormikSelect } from "~/features/common/form/FormikSelect";
import { FormikTextInput } from "~/features/common/form/FormikTextInput";
import { enumToOptions } from "~/features/common/helpers";
import FormInfoBox from "~/features/common/modals/FormInfoBox";
import { PRIVACY_NOTICE_REGION_RECORD } from "~/features/common/privacy-notice-regions";
import { useGetConfigurationSettingsQuery } from "~/features/config-settings/config-settings.slice";
import { useGetOnlyCountryLocationsQuery } from "~/features/locations/locations.slice";
import { getSelectedRegionIds } from "~/features/privacy-experience/form/helpers";
import {
Expand All @@ -33,6 +36,7 @@ interface WebsiteMonitorConfig
extends Omit<MonitorConfig, "datasource_params"> {
datasource_params?: WebsiteMonitorParams;
url: string;
llm_model_override?: string;
}

const FORM_COPY = `This monitor allows you to simulate and verify user consent actions, such as 'accept,' 'reject,' or 'opt-out,' on consent experiences. For each detected activity, the monitor will record whether it occurred before or after the configured user actions, ensuring compliance with user consent choices.`;
Expand Down Expand Up @@ -65,6 +69,20 @@ const ConfigureWebsiteMonitorForm = ({
}) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();
const { flags } = useFlags();

// Feature flag for LLM classification in website monitors
const llmClassifierFeatureEnabled = !!flags.alphaWebMonitorLlmClassification;

const { data: appConfig } = useGetConfigurationSettingsQuery(
{ api_set: false },
{ skip: !llmClassifierFeatureEnabled },
);

// Server-side LLM classifier capability
const serverSupportsLlmClassifier =
!!appConfig?.detection_discovery?.llm_classifier_enabled;

const integrationId = Array.isArray(router.query.id)
? router.query.id[0]
: (router.query.id as string);
Expand Down Expand Up @@ -97,6 +115,7 @@ const ConfigureWebsiteMonitorForm = ({
locations: [],
exclude_domains: [],
},
llm_model_override: monitor?.classify_params?.llm_model_override ?? "",
};

const handleSubmit = async (values: WebsiteMonitorConfig) => {
Expand All @@ -114,12 +133,20 @@ const ConfigureWebsiteMonitorForm = ({
execution_start_date: undefined,
};

// Build classify_params with llm_model_override if provided
const classifyParams = {
...(monitor?.classify_params || {}),
...(values.llm_model_override
? { llm_model_override: values.llm_model_override }
: { llm_model_override: undefined }),
};

const payload: WebsiteMonitorConfig = {
...monitor,
...values,
...executionInfo,
key: monitor?.key,
classify_params: monitor?.classify_params || {},
classify_params: classifyParams,
datasource_params: values.datasource_params || {},
connection_config_key: integrationId,
};
Expand Down Expand Up @@ -233,6 +260,33 @@ const ConfigureWebsiteMonitorForm = ({
onBlur={formik.handleBlur}
value={values.shared_config_id}
/>
{llmClassifierFeatureEnabled && (
<Form.Item
label={
<Tooltip
title={
!serverSupportsLlmClassifier
? "LLM classifier is currently disabled for this server. Contact Ethyca support to learn more."
: "Optionally specify a custom model to use for LLM classification of website assets"
}
>
<span>LLM model override</span>
</Tooltip>
}
>
<FormikTextInput
name="llm_model_override"
id="llm_model_override"
placeholder="e.g., openrouter/google/gemini-2.5-flash"
disabled={!serverSupportsLlmClassifier}
error={errors.llm_model_override}
touched={touched.llm_model_override}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
value={values.llm_model_override || ""}
/>
</Form.Item>
)}
<FormikSelect
name="execution_frequency"
id="execution_frequency"
Expand Down
14 changes: 14 additions & 0 deletions clients/admin-ui/src/flags.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,19 @@
"development": true,
"test": false,
"production": false
},
"alphaPrivacyRequestFieldConditions": {
"label": "Privacy request field conditions",
"description": "Allows to use privacy request fields as conditions for manual tasks",
"development": true,
"test": false,
"production": false
},
"alphaWebMonitorLlmClassification": {
"label": "Web monitor LLM classification",
"description": "Use LLM to classify website assets that cannot be identified by Compass",
"development": true,
"test": false,
"production": false
}
}
Loading