Skip to content

feat(llma): add evaluation reports frontend#54366

Open
andrewm4894 wants to merge 1 commit intoandy/llma-eval-reports-3-delivery-workflowsfrom
andy/llma-eval-reports-4-frontend
Open

feat(llma): add evaluation reports frontend#54366
andrewm4894 wants to merge 1 commit intoandy/llma-eval-reports-3-delivery-workflowsfrom
andy/llma-eval-reports-4-frontend

Conversation

@andrewm4894
Copy link
Copy Markdown
Member

@andrewm4894 andrewm4894 commented Apr 13, 2026

Problem

Users need a UI to configure evaluation report settings (frequency, delivery targets, prompt guidance) and view generated reports.

Part 4 of 5 in a stacked PR series. Depends on #54365.

Stack:

  1. Models + API (feat(llma): add evaluation report models and API #54363)
  2. Report Agent (feat(llma): add evaluation report agent #54364)
  3. Delivery + Workflows (feat(llma): add evaluation report delivery and temporal workflows #54365)
  4. Frontend (this PR)
  5. Wiring + Generated (feat(llma): wire up auto-create hook, generated types, and MCP tools #54367)

Changes

  • EvaluationReportConfig — form for frequency, threshold, delivery targets, prompt guidance
  • EvaluationReportsTab — history table with status badges for report runs
  • EvaluationReportViewer — rendered report content with sections and metrics
  • evaluationReportLogic — kea logic for API interactions and state management
  • Feature flag constant LLM_ANALYTICS_EVALUATIONS_REPORTS
  • All UI gated behind the feature flag

How did you test this code?

Agent-authored code. Unit/integration tests are not included for frontend components in this PR. Manual testing was performed during development on the reference branch.

No

Docs update

skip-inkeep-docs

🤖 LLM context

Agent-assisted PR stack creation from a single reference branch.

Copy link
Copy Markdown
Member Author

andrewm4894 commented Apr 13, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@github-actions
Copy link
Copy Markdown
Contributor

Hey @andrewm4894! 👋\nThis pull request seems to contain no description. Please add useful context, rationale, and/or any other information that will help make sense of this change now and in the distant Mars-based future.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 13, 2026

Size Change: +29.5 kB (+0.02%)

Total Size: 129 MB

Filename Size Change
frontend/dist/exporter.js 20.9 MB +4.19 kB (+0.02%)
frontend/dist/LLMAnalyticsEvaluation 58.7 kB +17 kB (+40.8%) 🚨
frontend/dist/render-query 20.6 MB +4.19 kB (+0.02%)
frontend/dist/render-query.js 20.6 MB +4.19 kB (+0.02%)
ℹ️ View Unchanged
Filename Size Change
frontend/dist/368Hedgehogs 5.26 kB 0 B
frontend/dist/abap 14.2 kB 0 B
frontend/dist/AccountSocialConnected 1.62 kB 0 B
frontend/dist/Action 23.2 kB 0 B
frontend/dist/Actions 1.02 kB 0 B
frontend/dist/AdvancedActivityLogsScene 34 kB 0 B
frontend/dist/AgenticAuthorize 5.25 kB 0 B
frontend/dist/apex 3.95 kB 0 B
frontend/dist/ApprovalDetail 16.2 kB 0 B
frontend/dist/array.full.es5.js 332 kB 0 B
frontend/dist/array.full.js 427 kB 0 B
frontend/dist/array.js 183 kB 0 B
frontend/dist/AsyncMigrations 13.1 kB 0 B
frontend/dist/AuthorizationStatus 716 B 0 B
frontend/dist/azcli 846 B 0 B
frontend/dist/bat 1.84 kB 0 B
frontend/dist/BatchExportScene 60.3 kB 0 B
frontend/dist/bicep 2.55 kB 0 B
frontend/dist/Billing 493 B 0 B
frontend/dist/BillingSection 20.8 kB 0 B
frontend/dist/BoxPlot 5.04 kB 0 B
frontend/dist/browserAll-0QZMN1W2 37.4 kB 0 B
frontend/dist/ButtonPrimitives 562 B 0 B
frontend/dist/CalendarHeatMap 4.79 kB 0 B
frontend/dist/cameligo 2.18 kB 0 B
frontend/dist/changeRequestsLogic 544 B 0 B
frontend/dist/CLIAuthorize 11.3 kB 0 B
frontend/dist/CLILive 3.97 kB 0 B
frontend/dist/clojure 9.64 kB 0 B
frontend/dist/coffee 3.59 kB 0 B
frontend/dist/Cohort 23.2 kB 0 B
frontend/dist/CohortCalculationHistory 6.22 kB 0 B
frontend/dist/Cohorts 9.39 kB 0 B
frontend/dist/ConfirmOrganization 4.48 kB 0 B
frontend/dist/conversations.js 65.8 kB 0 B
frontend/dist/Coupons 720 B 0 B
frontend/dist/cpp 5.3 kB 0 B
frontend/dist/Create 829 B 0 B
frontend/dist/crisp-chat-integration.js 1.88 kB 0 B
frontend/dist/csharp 4.52 kB 0 B
frontend/dist/csp 1.42 kB 0 B
frontend/dist/css 4.51 kB 0 B
frontend/dist/cssMode 4.15 kB 0 B
frontend/dist/CustomCssScene 3.55 kB 0 B
frontend/dist/CustomerAnalyticsConfigurationScene 1.99 kB 0 B
frontend/dist/CustomerAnalyticsScene 26.4 kB 0 B
frontend/dist/CustomerJourneyBuilderScene 1.69 kB 0 B
frontend/dist/CustomerJourneyTemplatesScene 7.39 kB 0 B
frontend/dist/customizations.full.js 17.9 kB 0 B
frontend/dist/CyclotronJobInputAssignee 1.32 kB 0 B
frontend/dist/CyclotronJobInputTicketTags 711 B 0 B
frontend/dist/cypher 3.38 kB 0 B
frontend/dist/dart 4.25 kB 0 B
frontend/dist/Dashboard 1.11 kB 0 B
frontend/dist/Dashboards 23.1 kB 0 B
frontend/dist/DataManagementScene 646 B 0 B
frontend/dist/DataPipelinesNewScene 2.28 kB 0 B
frontend/dist/DataWarehouseScene 1.21 kB -50 B (-3.97%)
frontend/dist/DataWarehouseSourceScene 634 B 0 B
frontend/dist/Deactivated 1.13 kB 0 B
frontend/dist/dead-clicks-autocapture.js 13.1 kB 0 B
frontend/dist/DeadLetterQueue 5.38 kB 0 B
frontend/dist/DebugScene 20 kB 0 B
frontend/dist/decompressionWorker 2.85 kB 0 B
frontend/dist/decompressionWorker.js 2.85 kB 0 B
frontend/dist/DefinitionEdit 7.11 kB 0 B
frontend/dist/DefinitionView 22.7 kB 0 B
frontend/dist/DestinationsScene 2.67 kB 0 B
frontend/dist/dist 575 B 0 B
frontend/dist/dockerfile 1.87 kB 0 B
frontend/dist/EarlyAccessFeature 753 B 0 B
frontend/dist/EarlyAccessFeatures 2.84 kB 0 B
frontend/dist/ecl 5.33 kB 0 B
frontend/dist/EditorScene 896 B 0 B
frontend/dist/elixir 10.3 kB 0 B
frontend/dist/elk.bundled 1.44 MB 0 B
frontend/dist/EmailMFAVerify 2.98 kB 0 B
frontend/dist/EndpointScene 37.5 kB 0 B
frontend/dist/EndpointsScene 22.1 kB 0 B
frontend/dist/ErrorTrackingConfigurationScene 2.2 kB 0 B
frontend/dist/ErrorTrackingIssueFingerprintsScene 6.98 kB 0 B
frontend/dist/ErrorTrackingIssueScene 81.7 kB 0 B
frontend/dist/ErrorTrackingScene 12.9 kB 0 B
frontend/dist/EvaluationTemplates 575 B 0 B
frontend/dist/EventsScene 2.46 kB 0 B
frontend/dist/exception-autocapture.js 11.8 kB 0 B
frontend/dist/Experiment 210 kB 0 B
frontend/dist/Experiments 17.7 kB 0 B
frontend/dist/exporter 20.9 MB 0 B
frontend/dist/ExportsScene 3.86 kB 0 B
frontend/dist/FeatureFlag 128 kB 0 B
frontend/dist/FeatureFlags 606 B 0 B
frontend/dist/FeatureFlagTemplatesScene 7.03 kB 0 B
frontend/dist/FlappyHog 5.78 kB 0 B
frontend/dist/flow9 1.8 kB 0 B
frontend/dist/freemarker2 16.7 kB 0 B
frontend/dist/fsharp 2.98 kB 0 B
frontend/dist/go 2.65 kB 0 B
frontend/dist/graphql 2.26 kB 0 B
frontend/dist/Group 14.4 kB 0 B
frontend/dist/Groups 3.91 kB 0 B
frontend/dist/GroupsNew 7.34 kB 0 B
frontend/dist/handlebars 7.34 kB 0 B
frontend/dist/hcl 3.59 kB 0 B
frontend/dist/HealthCategoryDetailScene 7.23 kB 0 B
frontend/dist/HealthScene 10.3 kB 0 B
frontend/dist/HeatmapNewScene 4.16 kB 0 B
frontend/dist/HeatmapRecordingScene 3.92 kB 0 B
frontend/dist/HeatmapScene 5.88 kB 0 B
frontend/dist/HeatmapsScene 3.88 kB 0 B
frontend/dist/hls 394 kB 0 B
frontend/dist/HogFunctionScene 58.7 kB 0 B
frontend/dist/HogRepl 7.37 kB 0 B
frontend/dist/html 5.58 kB 0 B
frontend/dist/htmlMode 4.62 kB 0 B
frontend/dist/image-blob-reduce.esm 49.4 kB 0 B
frontend/dist/InboxScene 59.7 kB 0 B
frontend/dist/index 311 kB 0 B
frontend/dist/index.js 311 kB 0 B
frontend/dist/ini 1.1 kB 0 B
frontend/dist/InsightOptions 5.41 kB 0 B
frontend/dist/InsightScene 28.9 kB 0 B
frontend/dist/IntegrationsRedirect 733 B 0 B
frontend/dist/intercom-integration.js 1.93 kB 0 B
frontend/dist/InviteSignup 14.4 kB 0 B
frontend/dist/java 3.22 kB 0 B
frontend/dist/javascript 985 B 0 B
frontend/dist/jsonMode 13.9 kB 0 B
frontend/dist/julia 7.22 kB 0 B
frontend/dist/kotlin 3.4 kB 0 B
frontend/dist/lazy 150 kB 0 B
frontend/dist/LegacyPluginScene 26.6 kB 0 B
frontend/dist/LemonTextAreaMarkdown 502 B 0 B
frontend/dist/less 3.9 kB 0 B
frontend/dist/lexon 2.44 kB 0 B
frontend/dist/lib 2.22 kB 0 B
frontend/dist/Link 468 B 0 B
frontend/dist/LinkScene 24.8 kB 0 B
frontend/dist/LinksScene 4.19 kB 0 B
frontend/dist/liquid 4.53 kB 0 B
frontend/dist/LiveDebugger 19.1 kB 0 B
frontend/dist/LiveEventsTable 2.98 kB 0 B
frontend/dist/LLMAnalyticsClusterScene 15.7 kB 0 B
frontend/dist/LLMAnalyticsClustersScene 43.1 kB 0 B
frontend/dist/LLMAnalyticsDatasetScene 19.7 kB 0 B
frontend/dist/LLMAnalyticsDatasetsScene 3.28 kB 0 B
frontend/dist/LLMAnalyticsEvaluationsScene 29.5 kB 0 B
frontend/dist/LLMAnalyticsPlaygroundScene 36.3 kB 0 B
frontend/dist/LLMAnalyticsScene 117 kB 0 B
frontend/dist/LLMAnalyticsSessionScene 13.4 kB 0 B
frontend/dist/LLMAnalyticsTraceScene 127 kB 0 B
frontend/dist/LLMAnalyticsUsers 526 B 0 B
frontend/dist/LLMASessionFeedbackDisplay 4.83 kB 0 B
frontend/dist/LLMPromptScene 20.6 kB 0 B
frontend/dist/LLMPromptsScene 4.21 kB 0 B
frontend/dist/Login 8.57 kB 0 B
frontend/dist/Login2FA 4.2 kB 0 B
frontend/dist/logs.js 38.5 kB 0 B
frontend/dist/LogsScene 11.3 kB 0 B
frontend/dist/lua 2.11 kB 0 B
frontend/dist/m3 2.81 kB 0 B
frontend/dist/main 819 kB 0 B
frontend/dist/ManagedMigration 14 kB 0 B
frontend/dist/markdown 3.79 kB 0 B
frontend/dist/MarketingAnalyticsScene 39.7 kB 0 B
frontend/dist/MaterializedColumns 10.2 kB 0 B
frontend/dist/Max 835 B 0 B
frontend/dist/mdx 5.39 kB 0 B
frontend/dist/memlens.lib.bundle 27.8 kB 0 B
frontend/dist/MessageTemplate 16.3 kB 0 B
frontend/dist/MetricsScene 828 B 0 B
frontend/dist/mips 2.58 kB 0 B
frontend/dist/ModelsScene 13.6 kB 0 B
frontend/dist/MonacoDiffEditor 403 B 0 B
frontend/dist/monacoEditorWorker 288 kB 0 B
frontend/dist/monacoEditorWorker.js 288 kB 0 B
frontend/dist/monacoJsonWorker 419 kB 0 B
frontend/dist/monacoJsonWorker.js 419 kB 0 B
frontend/dist/monacoTsWorker 7.02 MB 0 B
frontend/dist/monacoTsWorker.js 7.02 MB 0 B
frontend/dist/MoveToPostHogCloud 4.46 kB 0 B
frontend/dist/msdax 4.91 kB 0 B
frontend/dist/mysql 11.3 kB 0 B
frontend/dist/NavTabChat 4.68 kB 0 B
frontend/dist/NewSourceWizard 724 B 0 B
frontend/dist/NewTabScene 681 B 0 B
frontend/dist/NodeDetailScene 16.3 kB 0 B
frontend/dist/NotebookCanvasScene 3.2 kB 0 B
frontend/dist/NotebookPanel 5.21 kB 0 B
frontend/dist/NotebookScene 8.21 kB 0 B
frontend/dist/NotebooksScene 7.58 kB 0 B
frontend/dist/OAuthAuthorize 573 B 0 B
frontend/dist/objective-c 2.41 kB 0 B
frontend/dist/Onboarding 699 kB 0 B
frontend/dist/OnboardingCouponRedemption 1.2 kB 0 B
frontend/dist/pascal 2.99 kB 0 B
frontend/dist/pascaligo 2 kB 0 B
frontend/dist/passkeyLogic 484 B 0 B
frontend/dist/PasswordReset 4.32 kB 0 B
frontend/dist/PasswordResetComplete 2.94 kB 0 B
frontend/dist/perl 8.25 kB 0 B
frontend/dist/PersonScene 16.1 kB 0 B
frontend/dist/PersonsScene 4.68 kB 0 B
frontend/dist/pgsql 13.5 kB 0 B
frontend/dist/php 8.02 kB 0 B
frontend/dist/PipelineStatusScene 6.22 kB 0 B
frontend/dist/pla 1.67 kB 0 B
frontend/dist/posthog 137 kB 0 B
frontend/dist/postiats 7.86 kB 0 B
frontend/dist/powerquery 16.9 kB 0 B
frontend/dist/powershell 3.27 kB 0 B
frontend/dist/PreflightCheck 5.53 kB 0 B
frontend/dist/product-tours.js 115 kB 0 B
frontend/dist/ProductTour 273 kB 0 B
frontend/dist/ProductTours 4.68 kB 0 B
frontend/dist/ProjectHomepage 24.7 kB 0 B
frontend/dist/protobuf 9.05 kB 0 B
frontend/dist/pug 4.82 kB 0 B
frontend/dist/python 4.76 kB 0 B
frontend/dist/qsharp 3.19 kB 0 B
frontend/dist/QueryPerformance 3.44 kB 0 B
frontend/dist/r 3.12 kB 0 B
frontend/dist/razor 9.35 kB 0 B
frontend/dist/recorder-v2.js 111 kB 0 B
frontend/dist/recorder.js 111 kB 0 B
frontend/dist/redis 3.55 kB 0 B
frontend/dist/redshift 11.8 kB 0 B
frontend/dist/RegionMap 29.4 kB 0 B
frontend/dist/ResourceTransfer 9.17 kB 0 B
frontend/dist/restructuredtext 3.9 kB 0 B
frontend/dist/RevenueAnalyticsScene 25.6 kB 0 B
frontend/dist/ruby 8.5 kB 0 B
frontend/dist/rust 4.16 kB 0 B
frontend/dist/SavedInsights 664 B 0 B
frontend/dist/sb 1.82 kB 0 B
frontend/dist/scala 7.32 kB 0 B
frontend/dist/scheme 1.76 kB 0 B
frontend/dist/scss 6.41 kB 0 B
frontend/dist/SdkDoctorScene 9.4 kB 0 B
frontend/dist/SessionAttributionExplorerScene 6.62 kB 0 B
frontend/dist/SessionGroupSummariesTable 4.62 kB 0 B
frontend/dist/SessionGroupSummaryScene 17 kB 0 B
frontend/dist/SessionProfileScene 15.8 kB 0 B
frontend/dist/SessionRecordingDetail 1.73 kB 0 B
frontend/dist/SessionRecordingFilePlaybackScene 4.46 kB 0 B
frontend/dist/SessionRecordings 742 B 0 B
frontend/dist/SessionRecordingsKiosk 8.84 kB 0 B
frontend/dist/SessionRecordingsPlaylistScene 4.14 kB 0 B
frontend/dist/SessionRecordingsSettingsScene 1.9 kB 0 B
frontend/dist/SessionsScene 3.86 kB 0 B
frontend/dist/SettingsScene 2.98 kB 0 B
frontend/dist/SharedMetric 4.83 kB 0 B
frontend/dist/SharedMetrics 549 B 0 B
frontend/dist/shell 3.07 kB 0 B
frontend/dist/SignupContainer 24.5 kB 0 B
frontend/dist/Site 1.18 kB 0 B
frontend/dist/solidity 18.6 kB 0 B
frontend/dist/sophia 2.76 kB 0 B
frontend/dist/SourcesScene 5.96 kB 0 B
frontend/dist/sourceWizardLogic 662 B 0 B
frontend/dist/sparql 2.55 kB 0 B
frontend/dist/sql 10.3 kB 0 B
frontend/dist/SqlVariableEditScene 7.24 kB 0 B
frontend/dist/st 7.4 kB 0 B
frontend/dist/StartupProgram 21.2 kB 0 B
frontend/dist/SubscriptionsScene 16.4 kB 0 B
frontend/dist/SupportSettingsScene 1.16 kB 0 B
frontend/dist/SupportTicketScene 23.4 kB 0 B
frontend/dist/SupportTicketsScene 733 B 0 B
frontend/dist/Survey 848 B 0 B
frontend/dist/SurveyFormBuilder 1.54 kB 0 B
frontend/dist/Surveys 18.2 kB 0 B
frontend/dist/surveys.js 90 kB 0 B
frontend/dist/SurveyWizard 64.2 kB 0 B
frontend/dist/swift 5.26 kB 0 B
frontend/dist/SystemStatus 16.8 kB 0 B
frontend/dist/systemverilog 7.61 kB 0 B
frontend/dist/TaskDetailScene 21.5 kB 0 B
frontend/dist/TaskTracker 13.2 kB 0 B
frontend/dist/tcl 3.57 kB 0 B
frontend/dist/TextCardMarkdownEditor 11 kB 0 B
frontend/dist/toolbar 10.7 MB 0 B
frontend/dist/toolbar.js 10.7 MB -40 B (0%)
frontend/dist/ToolbarLaunch 2.52 kB 0 B
frontend/dist/tracing-headers.js 1.74 kB 0 B
frontend/dist/TracingScene 29.4 kB 0 B
frontend/dist/TransformationsScene 1.91 kB 0 B
frontend/dist/tsMode 24 kB 0 B
frontend/dist/twig 5.97 kB 0 B
frontend/dist/TwoFactorReset 3.98 kB 0 B
frontend/dist/typescript 240 B 0 B
frontend/dist/typespec 2.82 kB 0 B
frontend/dist/Unsubscribe 1.62 kB 0 B
frontend/dist/UserInterview 4.53 kB 0 B
frontend/dist/UserInterviews 2.01 kB 0 B
frontend/dist/vb 5.79 kB 0 B
frontend/dist/VercelConnect 4.95 kB 0 B
frontend/dist/VercelLinkError 1.91 kB 0 B
frontend/dist/VerifyEmail 4.48 kB 0 B
frontend/dist/vimMode 211 kB 0 B
frontend/dist/VisualReviewRunScene 18.6 kB 0 B
frontend/dist/VisualReviewRunsScene 6.16 kB 0 B
frontend/dist/VisualReviewSettingsScene 10.8 kB 0 B
frontend/dist/web-vitals.js 6.39 kB 0 B
frontend/dist/WebAnalyticsScene 5.77 kB 0 B
frontend/dist/WebGLRenderer-DYjOwNoG 60.3 kB 0 B
frontend/dist/WebGPURenderer-B_wkl_Ja 36.3 kB 0 B
frontend/dist/WebScriptsScene 2.54 kB 0 B
frontend/dist/webworkerAll-puPV1rBA 324 B 0 B
frontend/dist/wgsl 7.34 kB 0 B
frontend/dist/Wizard 4.45 kB 0 B
frontend/dist/WorkflowScene 102 kB 0 B
frontend/dist/WorkflowsScene 46.9 kB 0 B
frontend/dist/WorldMap 4.73 kB 0 B
frontend/dist/xml 2.98 kB 0 B
frontend/dist/yaml 4.6 kB 0 B

compressed-size-action

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 13, 2026

Prompt To Fix All With AI
This is a comment left during a code review.
Path: products/llm_analytics/frontend/evaluations/evaluationReportLogic.ts
Line: 105-108

Comment:
**Client-side filtering on paginated endpoint silently drops results**

`loadReports` fetches the full list without a server-side `evaluation` filter, then filters the first page in memory. Because the response is paginated (`response.results`), any reports that fall outside the first page are silently ignored — so an evaluation's schedule could disappear from the UI if it isn't on page 1.

Pass the evaluation ID as a query parameter instead:

```suggestion
                    const response = await api.get(
                        `api/environments/${values.currentTeamId}/llm_analytics/evaluation_reports/?evaluation=${props.evaluationId}`
                    )
                    return response.results || []
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: products/llm_analytics/frontend/evaluations/components/EvaluationReportConfig.tsx
Line: 99

Comment:
**Double non-null assertion can pass `undefined` to `SlackChannelPicker`**

`integrations!.find(...)!` — if the selected integration ID no longer exists in the list (e.g. it was disconnected), `find()` returns `undefined`, and the `!` doesn't prevent that from being forwarded to `SlackChannelPicker.integration`. The component receives an `undefined` where it expects an object, which will likely throw.

More safely, render nothing if the integration isn't found:

```ts
const selectedIntegration = integrations?.find((i) => i.id === slackIntegrationId)
// then: {selectedIntegration && <SlackChannelPicker ... integration={selectedIntegration} />}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: products/llm_analytics/frontend/evaluations/components/EvaluationReportViewer.tsx
Line: 214

Comment:
**Title rendered twice in non-compact mode**

In non-compact mode `content.title` is already shown in the `<h2>` at line 170 (alongside the status badge). This `<h3>` then renders the same title again below the divider, so the title appears twice. The compact block (lines 188–212) already handles the title for compact mode.

If the intent is to show a standalone title below the divider only in compact mode, the condition needs to be flipped:

```suggestion
            {compact && content.title && <h3 className="font-semibold text-sm mb-2">{content.title}</h3>}
```

But since the compact block at line 190 already renders `content.title` as an `<h3>`, this line may not be needed at all.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: products/llm_analytics/frontend/evaluations/components/EvaluationReportConfig.tsx
Line: 188-194

Comment:
**`useState` in a component backed by a kea logic**

PostHog's coding conventions say to avoid React hooks when a kea logic file exists ("write all business logic there, avoid React hooks at all costs"). `ExistingReportConfig` uses seven `useState` calls for form fields that mirror the persisted `activeReport` shape — this state belongs in `evaluationReportLogic` (or a thin sibling logic) where it can be observed, tested, and reset predictably.

The `useEffect` that seeds these fields from `activeReport` is also a common kea antipattern; a `listeners` handler on `loadReportsSuccess` / `updateReportSuccess` could set kea reducers instead.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: products/llm_analytics/frontend/evaluations/components/EvaluationReportConfig.tsx
Line: 327-342

Comment:
**Targets-building logic duplicated (OnceAndOnlyOnce)**

The code that builds the `targets: EvaluationReportDeliveryTarget[]` array is repeated verbatim here in `handleSave` and again inside the IIFE `onClick` for the update path (lines ~349–362). Extract it to a helper to keep the two code paths in sync — the two implementations are already subtly different (one explicitly passes `trigger_threshold: null`, the other omits the key entirely).

```ts
const buildTargets = (): EvaluationReportDeliveryTarget[] => {
    const targets: EvaluationReportDeliveryTarget[] = []
    if (hasEmail) {
        targets.push({ type: 'email', value: emailValue.trim() })
    }
    if (hasSlack) {
        targets.push({ type: 'slack', integration_id: slackIntegrationId!, channel: slackChannelValue })
    }
    return targets
}
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat(llma): add evaluation reports front..." | Re-trigger Greptile

Comment on lines +105 to +108
const response = await api.get(
`api/environments/${values.currentTeamId}/llm_analytics/evaluation_reports/`
)
return (response.results || []).filter((r: EvaluationReport) => r.evaluation === props.evaluationId)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Client-side filtering on paginated endpoint silently drops results

loadReports fetches the full list without a server-side evaluation filter, then filters the first page in memory. Because the response is paginated (response.results), any reports that fall outside the first page are silently ignored — so an evaluation's schedule could disappear from the UI if it isn't on page 1.

Pass the evaluation ID as a query parameter instead:

Suggested change
const response = await api.get(
`api/environments/${values.currentTeamId}/llm_analytics/evaluation_reports/`
)
return (response.results || []).filter((r: EvaluationReport) => r.evaluation === props.evaluationId)
const response = await api.get(
`api/environments/${values.currentTeamId}/llm_analytics/evaluation_reports/?evaluation=${props.evaluationId}`
)
return response.results || []
Prompt To Fix With AI
This is a comment left during a code review.
Path: products/llm_analytics/frontend/evaluations/evaluationReportLogic.ts
Line: 105-108

Comment:
**Client-side filtering on paginated endpoint silently drops results**

`loadReports` fetches the full list without a server-side `evaluation` filter, then filters the first page in memory. Because the response is paginated (`response.results`), any reports that fall outside the first page are silently ignored — so an evaluation's schedule could disappear from the UI if it isn't on page 1.

Pass the evaluation ID as a query parameter instead:

```suggestion
                    const response = await api.get(
                        `api/environments/${values.currentTeamId}/llm_analytics/evaluation_reports/?evaluation=${props.evaluationId}`
                    )
                    return response.results || []
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the client-side filter on a paginated endpoint is indeed wrong. I'll add server-side ?evaluation= filtering to the viewset's safely_get_queryset and pass it from the frontend. Fix incoming.

<SlackChannelPicker
value={slackChannelValue}
onChange={(val) => onSlackChannelChange(val || '')}
integration={integrations!.find((i) => i.id === slackIntegrationId)!}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Double non-null assertion can pass undefined to SlackChannelPicker

integrations!.find(...)! — if the selected integration ID no longer exists in the list (e.g. it was disconnected), find() returns undefined, and the ! doesn't prevent that from being forwarded to SlackChannelPicker.integration. The component receives an undefined where it expects an object, which will likely throw.

More safely, render nothing if the integration isn't found:

const selectedIntegration = integrations?.find((i) => i.id === slackIntegrationId)
// then: {selectedIntegration && <SlackChannelPicker ... integration={selectedIntegration} />}
Prompt To Fix With AI
This is a comment left during a code review.
Path: products/llm_analytics/frontend/evaluations/components/EvaluationReportConfig.tsx
Line: 99

Comment:
**Double non-null assertion can pass `undefined` to `SlackChannelPicker`**

`integrations!.find(...)!` — if the selected integration ID no longer exists in the list (e.g. it was disconnected), `find()` returns `undefined`, and the `!` doesn't prevent that from being forwarded to `SlackChannelPicker.integration`. The component receives an `undefined` where it expects an object, which will likely throw.

More safely, render nothing if the integration isn't found:

```ts
const selectedIntegration = integrations?.find((i) => i.id === slackIntegrationId)
// then: {selectedIntegration && <SlackChannelPicker ... integration={selectedIntegration} />}
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid — the double ! is unsafe if the integration was disconnected. I'll guard with a conditional render. Fix incoming.

</div>
)}

{!compact && content.title && <h3 className="font-semibold text-sm mb-2">{content.title}</h3>}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Title rendered twice in non-compact mode

In non-compact mode content.title is already shown in the <h2> at line 170 (alongside the status badge). This <h3> then renders the same title again below the divider, so the title appears twice. The compact block (lines 188–212) already handles the title for compact mode.

If the intent is to show a standalone title below the divider only in compact mode, the condition needs to be flipped:

Suggested change
{!compact && content.title && <h3 className="font-semibold text-sm mb-2">{content.title}</h3>}
{compact && content.title && <h3 className="font-semibold text-sm mb-2">{content.title}</h3>}

But since the compact block at line 190 already renders content.title as an <h3>, this line may not be needed at all.

Prompt To Fix With AI
This is a comment left during a code review.
Path: products/llm_analytics/frontend/evaluations/components/EvaluationReportViewer.tsx
Line: 214

Comment:
**Title rendered twice in non-compact mode**

In non-compact mode `content.title` is already shown in the `<h2>` at line 170 (alongside the status badge). This `<h3>` then renders the same title again below the divider, so the title appears twice. The compact block (lines 188–212) already handles the title for compact mode.

If the intent is to show a standalone title below the divider only in compact mode, the condition needs to be flipped:

```suggestion
            {compact && content.title && <h3 className="font-semibold text-sm mb-2">{content.title}</h3>}
```

But since the compact block at line 190 already renders `content.title` as an `<h3>`, this line may not be needed at all.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good eye — this is a genuine duplicate. The <h2> at line 170 already renders the title in non-compact mode, so this <h3> at line 214 should be removed. Fix incoming.

Comment on lines +188 to +194
const [formEnabled, setFormEnabled] = useState(false)
const [frequency, setFrequency] = useState<EvaluationReportFrequency>('daily')
const [emailValue, setEmailValue] = useState('')
const [slackIntegrationId, setSlackIntegrationId] = useState<number | null>(null)
const [slackChannelValue, setSlackChannelValue] = useState('')
const [guidance, setGuidance] = useState('')
const [triggerThreshold, setTriggerThreshold] = useState(TRIGGER_THRESHOLD_DEFAULT)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 useState in a component backed by a kea logic

PostHog's coding conventions say to avoid React hooks when a kea logic file exists ("write all business logic there, avoid React hooks at all costs"). ExistingReportConfig uses seven useState calls for form fields that mirror the persisted activeReport shape — this state belongs in evaluationReportLogic (or a thin sibling logic) where it can be observed, tested, and reset predictably.

The useEffect that seeds these fields from activeReport is also a common kea antipattern; a listeners handler on loadReportsSuccess / updateReportSuccess could set kea reducers instead.

Prompt To Fix With AI
This is a comment left during a code review.
Path: products/llm_analytics/frontend/evaluations/components/EvaluationReportConfig.tsx
Line: 188-194

Comment:
**`useState` in a component backed by a kea logic**

PostHog's coding conventions say to avoid React hooks when a kea logic file exists ("write all business logic there, avoid React hooks at all costs"). `ExistingReportConfig` uses seven `useState` calls for form fields that mirror the persisted `activeReport` shape — this state belongs in `evaluationReportLogic` (or a thin sibling logic) where it can be observed, tested, and reset predictably.

The `useEffect` that seeds these fields from `activeReport` is also a common kea antipattern; a `listeners` handler on `loadReportsSuccess` / `updateReportSuccess` could set kea reducers instead.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — this should use kea reducers, not useState. The ExistingReportConfig form fields are essentially an "edit draft" that mirrors the persisted activeReport shape, which fits naturally into kea. I'll move this into the logic in a follow-up within this PR stack (PR 5 will need to touch this component anyway for wiring). Tracking it.

Comment on lines +327 to +342
const frequencyDirty = frequency !== activeReport.frequency
const targetsDirty =
emailValue.trim() !== currentEmail ||
slackIntegrationId !== currentSlackIntegrationId ||
slackChannelValue !== currentSlackChannel
const guidanceDirty = guidance !== currentGuidance
const thresholdDirty = frequency === 'every_n' && triggerThreshold !== currentThreshold
const isDirty = frequencyDirty || targetsDirty || guidanceDirty || thresholdDirty
const hasAnyTarget = hasEmail || hasSlack
return (
<div className="flex justify-end">
<LemonButton
type="primary"
size="small"
loading={reportsLoading}
disabledReason={
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Targets-building logic duplicated (OnceAndOnlyOnce)

The code that builds the targets: EvaluationReportDeliveryTarget[] array is repeated verbatim here in handleSave and again inside the IIFE onClick for the update path (lines ~349–362). Extract it to a helper to keep the two code paths in sync — the two implementations are already subtly different (one explicitly passes trigger_threshold: null, the other omits the key entirely).

const buildTargets = (): EvaluationReportDeliveryTarget[] => {
    const targets: EvaluationReportDeliveryTarget[] = []
    if (hasEmail) {
        targets.push({ type: 'email', value: emailValue.trim() })
    }
    if (hasSlack) {
        targets.push({ type: 'slack', integration_id: slackIntegrationId!, channel: slackChannelValue })
    }
    return targets
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: products/llm_analytics/frontend/evaluations/components/EvaluationReportConfig.tsx
Line: 327-342

Comment:
**Targets-building logic duplicated (OnceAndOnlyOnce)**

The code that builds the `targets: EvaluationReportDeliveryTarget[]` array is repeated verbatim here in `handleSave` and again inside the IIFE `onClick` for the update path (lines ~349–362). Extract it to a helper to keep the two code paths in sync — the two implementations are already subtly different (one explicitly passes `trigger_threshold: null`, the other omits the key entirely).

```ts
const buildTargets = (): EvaluationReportDeliveryTarget[] => {
    const targets: EvaluationReportDeliveryTarget[] = []
    if (hasEmail) {
        targets.push({ type: 'email', value: emailValue.trim() })
    }
    if (hasSlack) {
        targets.push({ type: 'slack', integration_id: slackIntegrationId!, channel: slackChannelValue })
    }
    return targets
}
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — the two paths already diverge subtly on trigger_threshold: null vs omission. I'll extract a buildTargets() helper. Fix incoming.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2eb779d0a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 393 to 395
ONBOARDING_WIZARD_PROMINENCE: 'onboarding-wizard-prominence', // owner: #team-growth multivariate=control,wizard-hero,wizard-tab,wizard-only
ONBOARDING_WIZARD_INSTALLATION_IMPROVED_COPY: 'onboarding-wizard-installation-improved-copy', // owner: @fercgomes #team-growth multivariate=control,test
ONBOARDING_MOBILE_INSTALL_HELPER: 'onboarding-mobile-install-helper', // owner: @fercgomes #team-growth multivariate=control,test — target $device_type=Mobile at the flag level
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restore onboarding value-prop feature flag constant

Removing ONBOARDING_DATA_WAREHOUSE_VALUE_PROP from FEATURE_FLAGS breaks the onboarding experiment lookup used in OnboardingDataWarehouseSourcesStep (useFeatureFlag('ONBOARDING_DATA_WAREHOUSE_VALUE_PROP', ...)), so the variant branches can no longer activate and users are forced into the fallback flow. If type-checking is bypassed this becomes a silent runtime regression (FEATURE_FLAGS[flag] resolves to undefined), and if type-checking runs it blocks the build.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a rebase artifact — the removal of ONBOARDING_DATA_WAREHOUSE_VALUE_PROP (and SURVEYS_TOOLBAR) is not intentional. These were dropped during a conflict resolution when stacking. I'll restore them. Fix incoming.

Comment on lines +221 to +225
// The backend auto-creates a default report config on eval creation.
// If the user configured delivery targets or custom settings, update
// the auto-created report after creation via the existing reports list.
if (targets.length > 0 || pendingConfig.reportPromptGuidance.trim()) {
actions.createReport({
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Create a report when saving pending schedule settings

This listener only calls createReport when delivery targets or guidance are present, based on the assumption that evaluation creation already provisions a default report; however, EvaluationViewSet.perform_create only saves/tracks the evaluation and does not create an EvaluationReport, so users who enable scheduled reports and set cadence/threshold without targets lose their settings and end up with no report configuration after creation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is incorrect — the backend does auto-create a default EvaluationReport on evaluation creation. See EvaluationViewSet.perform_create in products/llm_analytics/backend/api/evaluations.py (lines 235-246), which calls EvaluationReport.objects.create(...). The comment in the listener at line 221 explains this: the pending config listener only fires createReport when the user has configured delivery targets or custom guidance beyond the defaults.

@andrewm4894 andrewm4894 force-pushed the andy/llma-eval-reports-3-delivery-workflows branch from 394a1ad to b927375 Compare April 13, 2026 22:27
@andrewm4894 andrewm4894 force-pushed the andy/llma-eval-reports-4-frontend branch from 2eb779d to da19b28 Compare April 13, 2026 22:27
@andrewm4894 andrewm4894 force-pushed the andy/llma-eval-reports-3-delivery-workflows branch from b927375 to 6bc5ab1 Compare April 13, 2026 22:34
@andrewm4894 andrewm4894 force-pushed the andy/llma-eval-reports-4-frontend branch from da19b28 to 5859546 Compare April 13, 2026 22:34
@andrewm4894 andrewm4894 force-pushed the andy/llma-eval-reports-3-delivery-workflows branch from 6bc5ab1 to bc73add Compare April 13, 2026 22:39
@andrewm4894 andrewm4894 force-pushed the andy/llma-eval-reports-4-frontend branch 2 times, most recently from 409b90c to d8a5ccf Compare April 13, 2026 22:46
@andrewm4894 andrewm4894 force-pushed the andy/llma-eval-reports-3-delivery-workflows branch from bc73add to d286f52 Compare April 13, 2026 22:46
Add React components for evaluation report configuration, history
table, and report viewer. Includes kea logic for API interactions
and state management. All UI is gated behind the
LLM_ANALYTICS_EVALUATIONS_REPORTS feature flag.
@andrewm4894 andrewm4894 force-pushed the andy/llma-eval-reports-3-delivery-workflows branch from d286f52 to 7b533aa Compare April 13, 2026 22:48
@andrewm4894 andrewm4894 force-pushed the andy/llma-eval-reports-4-frontend branch from d8a5ccf to cb89a1b Compare April 13, 2026 22:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant