Skip to content
Open
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 @@ -8,7 +8,13 @@ import {PageFiltersContainer} from 'sentry/components/pageFilters/container';
import {PageFiltersStore} from 'sentry/components/pageFilters/store';
import {OrganizationStore} from 'sentry/stores/organizationStore';
import {ProjectsStore} from 'sentry/stores/projectsStore';
import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
import {WidgetBuilderV2} from 'sentry/views/dashboards/widgetBuilder/components/newWidgetBuilder';
import {
LLMContextProvider,
useLLMContext,
} from 'sentry/views/seerExplorer/contexts/llmContext';
import type {LLMContextSnapshot} from 'sentry/views/seerExplorer/contexts/llmContextTypes';

const organization = OrganizationFixture({
features: ['open-membership', 'visibility-explore-view'],
Expand Down Expand Up @@ -294,4 +300,92 @@ describe('NewWidgetBuilder', () => {

expect(await screen.findByText('Select a widget to preview')).toBeInTheDocument();
});

it('populates LLM context with builder state', async () => {
const {ContextCapture, getSnapshot} = makeContextCapture();

render(
<LLMContextProvider>
<ContextCapture />
<WidgetBuilderV2
isOpen
onClose={onCloseMock}
onSave={onSaveMock}
dashboard={DashboardFixture([])}
dashboardFilters={{}}
openWidgetTemplates={false}
setOpenWidgetTemplates={jest.fn()}
/>
</LLMContextProvider>,
{
organization,
initialRouterConfig: {
location: {
pathname: '/organizations/org-slug/dashboard/1/widget-builder/widget/new/',
query: {
displayType: DisplayType.LINE,
dataset: WidgetType.ERRORS,
title: 'My Widget',
yAxis: 'count()',
field: 'browser.name',
query: 'browser.name:Firefox',
},
},
},
}
);

await waitFor(() => {
const node = getSnapshot().nodes.find(n => n.nodeType === 'widget-builder');
expect(node).toBeDefined();
});

const node = getSnapshot().nodes.find(n => n.nodeType === 'widget-builder')!;
const data = node.data as Record<string, unknown>;
expect(data.mode).toBe('creating');
expect(data.title).toBe('My Widget');
expect(data.dataset).toBe(WidgetType.ERRORS);
expect(data.displayType).toBe(DisplayType.LINE);
expect(data.visualize).toEqual(['count()']);
expect(data.fields).toEqual(['browser.name']);
expect(data.query).toEqual(['browser.name:Firefox']);
});

it('does not register LLM context when the builder is closed', () => {
const {ContextCapture, getSnapshot} = makeContextCapture();

render(
<LLMContextProvider>
<ContextCapture />
<WidgetBuilderV2
isOpen={false}
onClose={onCloseMock}
onSave={onSaveMock}
dashboard={DashboardFixture([])}
dashboardFilters={{}}
openWidgetTemplates={false}
setOpenWidgetTemplates={jest.fn()}
/>
</LLMContextProvider>
);

const node = getSnapshot().nodes.find(n => n.nodeType === 'widget-builder');
expect(node).toBeUndefined();
});
});

function makeContextCapture() {
const ref: {current: (() => LLMContextSnapshot) | null} = {current: null};
function ContextCapture() {
const {getLLMContext} = useLLMContext();
ref.current = getLLMContext;
return null;
}
return {
ContextCapture,
getSnapshot: () => {
if (!ref.current) throw new Error('ContextCapture not mounted');
return ref.current();
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {IconClose} from 'sentry/icons';
import {t, tctCode} from 'sentry/locale';
import {trackAnalytics} from 'sentry/utils/analytics';
import {WidgetBuilderVersion} from 'sentry/utils/analytics/dashboardsAnalyticsEvents';
import {generateFieldAsString} from 'sentry/utils/discover/fields';
import {useLocation} from 'sentry/utils/useLocation';
import {useMedia} from 'sentry/utils/useMedia';
import {useOrganization} from 'sentry/utils/useOrganization';
Expand Down Expand Up @@ -70,7 +71,10 @@ import {useSegmentSpanWidgetState} from 'sentry/views/dashboards/widgetBuilder/h
import {convertBuilderStateToWidget} from 'sentry/views/dashboards/widgetBuilder/utils/convertBuilderStateToWidget';
import {convertWidgetToBuilderState} from 'sentry/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams';
import type {OnDataFetchedParams} from 'sentry/views/dashboards/widgetCard';
import {readableConditions} from 'sentry/views/dashboards/widgetCard/widgetLLMContext';
import {getTopNConvertedDefaultWidgets} from 'sentry/views/dashboards/widgetLibrary/data';
import {useLLMContext} from 'sentry/views/seerExplorer/contexts/llmContext';
import {registerLLMContext} from 'sentry/views/seerExplorer/contexts/registerLLMContext';

type WidgetBuilderSlideoutProps = {
dashboard: DashboardDetails;
Expand All @@ -86,7 +90,7 @@ type WidgetBuilderSlideoutProps = {
thresholdMetaState?: ThresholdMetaState;
};

export function WidgetBuilderSlideout({
function WidgetBuilderSlideoutInner({
onClose,
onSave,
onQueryConditionChange,
Expand All @@ -108,6 +112,23 @@ export function WidgetBuilderSlideout({
const [error, setError] = useState<Record<string, any>>({});
const theme = useTheme();
const isEditing = useIsEditingWidget();

// Push widget builder state into the LLM context tree for Seer Explorer.
useLLMContext({
contextHint:
'Sentry widget builder. The user is configuring a dashboard widget. visualize is the y-axis metrics (timeseries) or the aggregate (big number/table). fields are group-by columns (timeseries) or visible columns (table). query filters the data and sort controls ordering.',
mode: isEditing ? 'editing' : 'creating',
title: state.title,
description: state.description,
dataset: state.dataset,
displayType: state.displayType,
visualize: state.yAxis?.map(generateFieldAsString),
fields: state.fields?.map(generateFieldAsString),
query: state.query?.map(readableConditions),
sort: state.sort?.map(s => (s.kind === 'desc' ? `-${s.field}` : s.field)),
thresholds: state.thresholds,
legendAlias: state.legendAlias,
});
const source = useDashboardWidgetSource();
const {cacheBuilderState} = useCacheBuilderState();
const {setSegmentSpanBuilderState} = useSegmentSpanWidgetState();
Expand Down Expand Up @@ -508,6 +529,11 @@ export function WidgetBuilderSlideout({
);
}

export const WidgetBuilderSlideout = registerLLMContext(
'widget-builder',
WidgetBuilderSlideoutInner
);

function Section({children}: {children: React.ReactNode}) {
return (
<SectionWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* Known node types for the LLM context tree.
* Add new types here as new context-aware components are registered.
*/
export type LLMContextNodeType = 'chart' | 'dashboard' | 'widget';
export type LLMContextNodeType = 'chart' | 'dashboard' | 'widget' | 'widget-builder';

/**
* A single node in the flat registry.
Expand Down
6 changes: 5 additions & 1 deletion static/app/views/seerExplorer/hooks/useSeerExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ type SeerExplorerChatResponse = {
const POLL_INTERVAL = 500; // Poll every 500ms

/** Routes where the LLMContext tree provides structured page context. */
const STRUCTURED_CONTEXT_ROUTES = new Set(['/dashboard/:dashboardId/']);
const STRUCTURED_CONTEXT_ROUTES = new Set([
'/dashboard/:dashboardId/',
'/dashboard/:dashboardId/widget-builder/widget/new/',
'/dashboard/:dashboardId/widget-builder/widget/:widgetIndex/edit/',
]);
Comment thread
sentry[bot] marked this conversation as resolved.

const OPTIMISTIC_ASSISTANT_TEXTS = [
'Looking around...',
Expand Down
Loading