Add AI-powered chart generation to panel create/edit UX#1623
Conversation
Integrate the backend POST /dashboard/:dashboardId/widget/generate/:connectionId endpoint into the chart editor. Users can now describe a chart in natural language, select a table, and have AI generate the SQL query, chart type, and all configuration. The chart preview is always visible outside the collapsible manual config section. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Integrates the backend AI widget generation endpoint into the chart create/edit experience, adding a “Generate with AI” flow and restructuring the editor so chart preview is displayed outside the manual configuration section.
Changes:
- Added
DashboardsService.generateWidgetWithAi()to call/dashboard/:dashboardId/widget/generate/:connectionIdwithtableNamequery param. - Introduced
GeneratedPanelWithPositionmodel and applied AI response intoChartEditComponentstate. - Updated chart editor UI to include an AI generation section, collapsible manual configuration section, and an always-visible preview area when results exist.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/app/services/dashboards.service.ts | Adds an API wrapper method for AI widget generation. |
| frontend/src/app/models/saved-query.ts | Adds a new interface describing the AI-generated panel payload. |
| frontend/src/app/components/charts/chart-edit/chart-edit.component.ts | Loads AI prerequisites, calls AI generation, and maps AI output into editor state. |
| frontend/src/app/components/charts/chart-edit/chart-edit.component.html | Restructures the chart editor layout to add AI section + collapsible manual config + preview placement changes. |
| frontend/src/app/components/charts/chart-edit/chart-edit.component.css | Adds styling for the new AI/manual collapsible sections and updated layout. |
Comments suppressed due to low confidence (1)
frontend/src/app/components/charts/chart-edit/chart-edit.component.html:628
- The template appears to have mismatched
<div>nesting/closures near the bottom:chart-edit-layout/chart-edit-main/chart-edit-pageopen three wrapper<div>s, but only two are closed at the end of the file, and there is an extra closing</div>right after the@if (!loading())block. This will break template compilation and/or layout. Please re-check the wrapper structure so all opened elements are closed exactly once and the actions block is nested as intended.
</div>
}
</div>
@if (!loading()) {
<div class="actions">
<a mat-stroked-button [routerLink]="['/panels', connectionId()]" data-testid="cancel-button">Back</a>
<button mat-flat-button color="primary"
(click)="saveQuery()"
[disabled]="!canSave()"
data-testid="save-query-button">
@if (saving()) {
<mat-spinner diameter="18"></mat-spinner>
}
{{ saving() ? 'Saving...' : (isEditMode() ? 'Update query' : 'Save query') }}
</button>
</div>
}
</div>
</div>
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (result.widget_options) { | ||
| const opts = result.widget_options as Partial<ChartWidgetOptions>; | ||
|
|
||
| if (opts.label_column) this.labelColumn.set(opts.label_column); | ||
| if (opts.label_type) this.labelType.set(opts.label_type); | ||
|
|
||
| // Series | ||
| if (opts.series_column) { | ||
| this.seriesMode.set('column'); | ||
| this.seriesColumn.set(opts.series_column); | ||
| if (opts.value_column) this.seriesValueColumn.set(opts.value_column); | ||
| } else if (opts.series?.length) { | ||
| this.seriesMode.set('manual'); | ||
| this.seriesList.set([...opts.series]); | ||
| } else if (opts.value_column) { | ||
| this.seriesMode.set('manual'); | ||
| this.seriesList.set([{ value_column: opts.value_column }]); | ||
| } | ||
|
|
||
| // Display options | ||
| if (opts.stacked) this.stacked.set(true); | ||
| if (opts.horizontal) this.horizontal.set(true); | ||
| if (opts.show_data_labels) this.showDataLabels.set(true); | ||
|
|
||
| // Legend | ||
| if (opts.legend) { | ||
| if (opts.legend.show !== undefined) this.legendShow.set(opts.legend.show); | ||
| if (opts.legend.position) this.legendPosition.set(opts.legend.position); | ||
| } | ||
|
|
||
| // Color palette | ||
| if (opts.color_palette?.length) { | ||
| this.colorPalette.set([...opts.color_palette]); | ||
| } | ||
| } |
There was a problem hiding this comment.
The PR description says AI should generate “all configuration”, but _applyAiResponse currently ignores several supported ChartWidgetOptions fields (e.g. units, number_format, y_axis, x_axis, sort_by, limit). If the backend returns these, they will be dropped and the manual UI will show defaults instead of the generated config. Consider applying the full set of widget options here (or update the description/contract if partial application is intentional).
| <div class="ai-section-header" (click)="aiExpanded.set(!aiExpanded())"> | ||
| <div class="ai-section-title"> | ||
| <mat-icon class="ai-icon">auto_awesome</mat-icon> | ||
| <span>Generate with AI</span> | ||
| </div> | ||
| <mat-icon class="ai-toggle-icon">{{ aiExpanded() ? 'expand_less' : 'expand_more' }}</mat-icon> | ||
| </div> |
There was a problem hiding this comment.
The AI section header is a clickable <div> without button semantics, so it’s not keyboard-accessible and won’t announce expanded/collapsed state to assistive tech. Use a <button> (or mat-expansion-panel) for the header, or add role="button", tabindex="0", aria-expanded, and keyboard handlers (Enter/Space).
| <div class="manual-section-header" (click)="manualExpanded.set(!manualExpanded())"> | ||
| <div class="manual-section-title"> | ||
| <mat-icon>tune</mat-icon> | ||
| <span>Manual configuration</span> | ||
| </div> | ||
| <mat-icon class="manual-toggle-icon">{{ manualExpanded() ? 'expand_less' : 'expand_more' }}</mat-icon> | ||
| </div> | ||
|
|
||
| @if (manualExpanded()) { | ||
| <div class="manual-section-body"> |
There was a problem hiding this comment.
The “Manual configuration” collapse header is also implemented as a clickable <div> without button semantics, which prevents keyboard toggling and doesn’t expose expanded state for screen readers. Prefer a real <button>/mat-expansion-panel, or add role="button", tabindex="0", aria-expanded, and Enter/Space key handling.
| <div class="manual-section-header" (click)="manualExpanded.set(!manualExpanded())"> | |
| <div class="manual-section-title"> | |
| <mat-icon>tune</mat-icon> | |
| <span>Manual configuration</span> | |
| </div> | |
| <mat-icon class="manual-toggle-icon">{{ manualExpanded() ? 'expand_less' : 'expand_more' }}</mat-icon> | |
| </div> | |
| @if (manualExpanded()) { | |
| <div class="manual-section-body"> | |
| <button | |
| type="button" | |
| class="manual-section-header" | |
| (click)="manualExpanded.set(!manualExpanded())" | |
| aria-expanded="{{ manualExpanded() ? 'true' : 'false' }}" | |
| aria-controls="manual-section-body"> | |
| <div class="manual-section-title"> | |
| <mat-icon>tune</mat-icon> | |
| <span>Manual configuration</span> | |
| </div> | |
| <mat-icon class="manual-toggle-icon">{{ manualExpanded() ? 'expand_less' : 'expand_more' }}</mat-icon> | |
| </button> | |
| @if (manualExpanded()) { | |
| <div class="manual-section-body" id="manual-section-body"> |
| dashboardId: string, | ||
| connectionId: string, |
There was a problem hiding this comment.
generateWidgetWithAi uses a different parameter ordering than the rest of DashboardsService (dashboardId first vs connectionId first for create/update/delete). This makes it easy to call with swapped args and is inconsistent with the existing public API. Consider reordering the signature to connectionId, dashboardId, ... (while keeping URL construction the same) to match the established pattern in this service.
| dashboardId: string, | |
| connectionId: string, | |
| connectionId: string, | |
| dashboardId: string, |
| name: string; | ||
| description: string | null; | ||
| widget_type: DashboardWidgetType; | ||
| chart_type: string | null; |
There was a problem hiding this comment.
GeneratedPanelWithPosition.chart_type is typed as string | null, even though this file already defines ChartType and the UI code expects/validates against that union. Typing this as ChartType | null would improve type-safety and avoid repeated casting in consumers.
| chart_type: string | null; | |
| chart_type: ChartType | null; |
| } | ||
| } finally { | ||
| this.aiGenerating.set(false); | ||
| } | ||
|
|
||
| this.angulartics2.eventTrack.next({ | ||
| action: 'Charts: AI generation used', | ||
| }); | ||
| posthog.capture('Charts: AI generation used'); |
There was a problem hiding this comment.
Telemetry is recorded even when AI generation fails or returns null (the tracking calls run after the try/finally unconditionally). This will inflate usage metrics and make it hard to distinguish success vs failure. Move the Angulartics/PostHog tracking into the if (result) block (or add a separate failure event).
| } | |
| } finally { | |
| this.aiGenerating.set(false); | |
| } | |
| this.angulartics2.eventTrack.next({ | |
| action: 'Charts: AI generation used', | |
| }); | |
| posthog.capture('Charts: AI generation used'); | |
| this.angulartics2.eventTrack.next({ | |
| action: 'Charts: AI generation used', | |
| }); | |
| posthog.capture('Charts: AI generation used'); | |
| } | |
| } finally { | |
| this.aiGenerating.set(false); | |
| } |
| if (opts.stacked) this.stacked.set(true); | ||
| if (opts.horizontal) this.horizontal.set(true); | ||
| if (opts.show_data_labels) this.showDataLabels.set(true); |
There was a problem hiding this comment.
_applyAiResponse only sets stacked, horizontal, and showDataLabels to true when the AI response value is truthy. If a user previously enabled one of these and AI returns false, the UI state will not be reset, leading to stale config after regeneration. Use !== undefined checks and set the signal to the actual boolean value from opts instead of only setting true.
| if (opts.stacked) this.stacked.set(true); | |
| if (opts.horizontal) this.horizontal.set(true); | |
| if (opts.show_data_labels) this.showDataLabels.set(true); | |
| if (opts.stacked !== undefined) this.stacked.set(opts.stacked); | |
| if (opts.horizontal !== undefined) this.horizontal.set(opts.horizontal); | |
| if (opts.show_data_labels !== undefined) this.showDataLabels.set(opts.show_data_labels); |
Integrate the backend POST /dashboard/:dashboardId/widget/generate/:connectionId endpoint into the chart editor. Users can now describe a chart in natural language, select a table, and have AI generate the SQL query, chart type, and all configuration. The chart preview is always visible outside the collapsible manual config section.