From 7c872f0e8aa6d05061f546da98d94fe223192fa2 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:38:09 +0200 Subject: [PATCH] Alerting: Add feedback buttons for the new AI helpers (#108207) * Add alerting ai buttons for cloud * add tracking for ai buttons usage * Empty commit to trigger GitHub Actions * wip * add FrontendOnly in ff * update analytics folder * prettier * revert ff being frontend only * add feedback button in templates * review comments * address some review comments * refactor * revert change and remove comment * prettier * update betterer * remove unused property * prettier * update AIFeedbackComponent * add feedback buttons for alert rule generation with ai * remove tracking from oss * fix analytics imports * udpdate changes in go.mod go.sum * prettier * update translations * update workspace * fix betterer * address pr comments * move most of the ai feedback buttons code to enterprise * prettier * fix * address pr feedback --- .../src/types/featureToggles.gen.ts | 5 +++ pkg/services/featuremgmt/registry.go | 9 ++++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 ++ pkg/services/featuremgmt/toggles_gen.json | 15 +++++++ .../components/receivers/TemplateForm.tsx | 6 +++ .../components/receivers/TemplatePreview.tsx | 38 +++++++++++++++- .../alert-rule-form/AlertRuleForm.tsx | 5 +++ .../AI/addAIFeedbackButton.tsx | 43 +++++++++++++++++++ public/locales/en-US/grafana.json | 2 + 10 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 534d8064bbc..3b9875c801e 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -748,6 +748,11 @@ export interface FeatureToggles { */ alertingAIGenAlertRules?: boolean; /** + * Enable AI-generated feedback from the Grafana UI. + * @default false + */ + alertingAIFeedback?: boolean; + /** * Enable AI-improve alert rules labels and annotations. * @default false */ diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 3738ab4877a..d91e76998e2 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1278,6 +1278,15 @@ var ( HideFromDocs: true, Expression: "false", }, + { + Name: "alertingAIFeedback", + Description: "Enable AI-generated feedback from the Grafana UI.", + Stage: FeatureStageExperimental, + Owner: grafanaAlertingSquad, + HideFromAdminPage: true, + HideFromDocs: true, + Expression: "false", + }, { Name: "alertingAIImproveAlertRules", Description: "Enable AI-improve alert rules labels and annotations.", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 74b5f2a6fd9..689b0883024 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -167,6 +167,7 @@ crashDetection,experimental,@grafana/observability-traces-and-profiling,false,fa alertingUIOptimizeReducer,GA,@grafana/alerting-squad,false,false,true azureMonitorEnableUserAuth,GA,@grafana/partner-datasources,false,false,false alertingAIGenAlertRules,experimental,@grafana/alerting-squad,false,false,false +alertingAIFeedback,experimental,@grafana/alerting-squad,false,false,false alertingAIImproveAlertRules,experimental,@grafana/alerting-squad,false,false,false alertingAIGenTemplates,experimental,@grafana/alerting-squad,false,false,false alertingAIAnalyzeCentralStateHistory,experimental,@grafana/alerting-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index c94945edc9a..5ea9f0f1995 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -679,6 +679,10 @@ const ( // Enable AI-generated alert rules. FlagAlertingAIGenAlertRules = "alertingAIGenAlertRules" + // FlagAlertingAIFeedback + // Enable AI-generated feedback from the Grafana UI. + FlagAlertingAIFeedback = "alertingAIFeedback" + // FlagAlertingAIImproveAlertRules // Enable AI-improve alert rules labels and annotations. FlagAlertingAIImproveAlertRules = "alertingAIImproveAlertRules" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index a7cc676d47a..2b3a1c27cf9 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -89,6 +89,21 @@ "expression": "false" } }, + { + "metadata": { + "name": "alertingAIFeedback", + "resourceVersion": "1752820131286", + "creationTimestamp": "2025-07-18T06:28:51Z" + }, + "spec": { + "description": "Enable AI-generated feedback from the Grafana UI.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "hideFromAdminPage": true, + "hideFromDocs": true, + "expression": "false" + } + }, { "metadata": { "name": "alertingAIGenAlertRules", diff --git a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx index 0d104bd528f..6f0f358f011 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplateForm.tsx @@ -107,6 +107,9 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) const [payload, setPayload] = useState(defaultPayloadString); const [payloadFormatError, setPayloadFormatError] = useState(null); + // AI feedback state + const [aiGeneratedTemplate, setAiGeneratedTemplate] = useState(false); + const { isProvisioned } = useNotificationTemplateMetadata(originalTemplate); const originalTemplatePrefill: TemplateFormValues | undefined = originalTemplate ? { title: originalTemplate.title, content: originalTemplate.content } @@ -167,6 +170,7 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) const handleTemplateGenerated = (template: string) => { setValue('content', template); + setAiGeneratedTemplate(true); }; return ( @@ -352,6 +356,8 @@ export const TemplateForm = ({ originalTemplate, prefill, alertmanager }: Props) setPayloadFormatError={setPayloadFormatError} payloadFormatError={payloadFormatError} className={cx(styles.templatePreview, styles.minEditorSize)} + aiGeneratedTemplate={aiGeneratedTemplate} + setAiGeneratedTemplate={setAiGeneratedTemplate} /> )} diff --git a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx index 8745e12effd..fc6014a0e0e 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatePreview.tsx @@ -8,6 +8,7 @@ import { Trans, t } from '@grafana/i18n'; import { Alert, Box, Button, CodeEditor, useStyles2 } from '@grafana/ui'; import { TemplatePreviewErrors, TemplatePreviewResponse, TemplatePreviewResult } from '../../api/templateApi'; +import { AIFeedbackButtonComponent } from '../../enterprise-components/AI/addAIFeedbackButton'; import { stringifyErrorLike } from '../../utils/misc'; import { EditorColumnHeader } from '../contact-points/templates/EditorColumnHeader'; @@ -20,6 +21,8 @@ export function TemplatePreview({ payloadFormatError, setPayloadFormatError, className, + aiGeneratedTemplate, + setAiGeneratedTemplate, }: { payload: string; templateName: string; @@ -27,6 +30,8 @@ export function TemplatePreview({ payloadFormatError: string | null; setPayloadFormatError: (value: React.SetStateAction) => void; className?: string; + aiGeneratedTemplate?: boolean; + setAiGeneratedTemplate?: (aiGeneratedTemplate: boolean) => void; }) { const styles = useStyles2(getStyles); @@ -48,7 +53,10 @@ export function TemplatePreview({ disabled={isLoading} icon="sync" aria-label={t('alerting.template-preview.aria-label-refresh-preview', 'Refresh preview')} - onClick={onPreview} + onClick={() => { + onPreview(); + setAiGeneratedTemplate?.(false); + }} size="sm" variant="secondary" > @@ -56,6 +64,9 @@ export function TemplatePreview({ } /> +
+ +
{({ height }) =>
{previewToRender}
} @@ -163,6 +174,22 @@ const getStyles = (theme: GrafanaTheme2) => ({ errorText: css({ color: theme.colors.error.text, }), + feedbackContainer: css({ + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderTop: `1px solid ${theme.colors.border.medium}`, + backgroundColor: theme.colors.background.secondary, + minHeight: 'auto', + }), + emptyState: css({ + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + height: '100%', + color: theme.colors.text.secondary, + fontSize: theme.typography.bodySmall.fontSize, + }), }, }); @@ -174,10 +201,12 @@ export function getPreviewResults( // ERRORS IN JSON OR IN REQUEST (endpoint not available, for example) const previewErrorRequest = previewError ? stringifyErrorLike(previewError) : undefined; const errorToRender = payloadFormatError || previewErrorRequest; + const styles = useStyles2(getStyles); //PREVIEW : RESULTS AND ERRORS const previewResponseResults = data?.results ?? []; const previewResponseErrors = data?.errors; + const hasContent = previewResponseResults.length > 0 || previewResponseErrors || errorToRender; return ( <> @@ -187,7 +216,12 @@ export function getPreviewResults( )} {previewResponseErrors && } - {previewResponseResults && } + {previewResponseResults.length > 0 && } + {!hasContent && ( +
+ Add template content to see preview +
+ )} ); } diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index 0a16d0022a0..3d70f60f79a 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -40,6 +40,7 @@ import { RulerGroupUpdatedResponse, isGrafanaGroupUpdatedResponse, } from '../../../api/alertRuleModel'; +import { AIFeedbackButtonComponent } from '../../../enterprise-components/AI/addAIFeedbackButton'; import { useAddRuleToRuleGroup, useUpdateRuleInRuleGroup } from '../../../hooks/ruleGroup/useUpsertRuleFromRuleGroup'; import { defaultFormValuesForRuleType, @@ -244,6 +245,10 @@ export const AlertRuleForm = ({ existing, prefill, isManualRestore }: Props) => {/* Step 1 */} + + {/* AI Feedback: automatically shown for rules generated by AI */} + + {/* Step 2 */} {/* Step 3-4-5 */} diff --git a/public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx b/public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx new file mode 100644 index 00000000000..fae71645554 --- /dev/null +++ b/public/app/features/alerting/unified/enterprise-components/AI/addAIFeedbackButton.tsx @@ -0,0 +1,43 @@ +import { ComponentType, createElement } from 'react'; + +import { t } from '@grafana/i18n'; +import { withErrorBoundary } from '@grafana/ui'; +import { AIFeedbackOrigin } from 'app/extensions/alerting/AI/feedback/AIFeedbackComponent'; + +import { logError } from '../../Analytics'; + +export interface GenAIFeedbackButtonProps { + origin: AIFeedbackOrigin; + shouldShowFeedbackButton?: boolean; + // If true, the component will use the route detection to determine if the feedback should be shown + // this is necessary for example in alerting rule form, where the feedback button is shown in the same page as the rule form + useRouteDetection?: boolean; +} + +let InternalAIFeedbackButtonComponent: ComponentType | null = null; + +export const AIFeedbackButtonComponent: ComponentType = (props) => { + if (!InternalAIFeedbackButtonComponent) { + return null; + } + + // Wrap the component with error boundary + const WrappedComponent = withErrorBoundary(InternalAIFeedbackButtonComponent, { + title: t('alerting.ai.error-boundary.feedback-button', 'AI Feedback Button failed to load'), + style: 'alertbox', + errorLogger: logError, + }); + + // Provide default value for shouldShowFeedbackButton + const propsWithDefaults = { + shouldShowFeedbackButton: true, + useRouteDetection: false, + ...props, + }; + + return createElement(WrappedComponent, propsWithDefaults); +}; + +export function addAIFeedbackButton(component: ComponentType | null) { + InternalAIFeedbackButtonComponent = component; +} diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 180039f2b38..2d21e3f49bf 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -383,6 +383,7 @@ "ai": { "error-boundary": { "alert-rule-button": "AI Alert Rule Button failed to load", + "feedback-button": "AI Feedback Button failed to load", "improve-annotations-button": "AI Improve Annotations Button failed to load", "improve-labels-button": "AI Improve Labels Button failed to load", "template-button": "AI Template Button failed to load", @@ -2803,6 +2804,7 @@ }, "template-preview": { "aria-label-refresh-preview": "Refresh preview", + "empty-state": "Add template content to see preview", "label-preview": "Preview", "refresh": "Refresh" },