diff --git a/apps/api/src/controllers/export.controller.ts b/apps/api/src/controllers/export.controller.ts index ac622908..294f59c8 100644 --- a/apps/api/src/controllers/export.controller.ts +++ b/apps/api/src/controllers/export.controller.ts @@ -13,7 +13,7 @@ import { getSettingsForProject, } from '@openpanel/db'; import { ChartEngine } from '@openpanel/db'; -import { zChartEvent, zChartInputBase } from '@openpanel/validation'; +import { zChartEvent, zReport } from '@openpanel/validation'; import { omit } from 'ramda'; async function getProjectId( @@ -139,7 +139,7 @@ export async function events( }); } -const chartSchemeFull = zChartInputBase +const chartSchemeFull = zReport .pick({ breakdowns: true, interval: true, diff --git a/apps/api/src/utils/ai-tools.ts b/apps/api/src/utils/ai-tools.ts index e261b551..70f43d27 100644 --- a/apps/api/src/utils/ai-tools.ts +++ b/apps/api/src/utils/ai-tools.ts @@ -9,7 +9,7 @@ import { } from '@openpanel/db'; import { ChartEngine } from '@openpanel/db'; import { getCache } from '@openpanel/redis'; -import { zChartInputAI } from '@openpanel/validation'; +import { zReportInput } from '@openpanel/validation'; import { tool } from 'ai'; import { z } from 'zod'; @@ -27,7 +27,10 @@ export function getReport({ - ${chartTypes.metric} - ${chartTypes.bar} `, - parameters: zChartInputAI, + parameters: zReportInput.extend({ + startDate: z.string().describe('The start date for the report'), + endDate: z.string().describe('The end date for the report'), + }), execute: async (report) => { return { type: 'report', @@ -72,7 +75,10 @@ export function getConversionReport({ return tool({ description: 'Generate a report (a chart) for conversions between two actions a unique user took.', - parameters: zChartInputAI, + parameters: zReportInput.extend({ + startDate: z.string().describe('The start date for the report'), + endDate: z.string().describe('The end date for the report'), + }), execute: async (report) => { return { type: 'report', @@ -94,7 +100,10 @@ export function getFunnelReport({ return tool({ description: 'Generate a report (a chart) for funnel between two or more actions a unique user (session_id or profile_id) took.', - parameters: zChartInputAI, + parameters: zReportInput.extend({ + startDate: z.string().describe('The start date for the report'), + endDate: z.string().describe('The end date for the report'), + }), execute: async (report) => { return { type: 'report', diff --git a/apps/start/src/components/chat/chat-message.tsx b/apps/start/src/components/chat/chat-message.tsx index 5d08db9e..e380c247 100644 --- a/apps/start/src/components/chat/chat-message.tsx +++ b/apps/start/src/components/chat/chat-message.tsx @@ -1,6 +1,7 @@ import { Markdown } from '@/components/markdown'; import { cn } from '@/utils/cn'; -import { zChartInputAI } from '@openpanel/validation'; +import { zReport } from '@openpanel/validation'; +import { z } from 'zod'; import type { UIMessage } from 'ai'; import { Loader2Icon, UserIcon } from 'lucide-react'; import { Fragment, memo } from 'react'; @@ -77,7 +78,10 @@ export const ChatMessage = memo( const { result } = p.toolInvocation; if (result.type === 'report') { - const report = zChartInputAI.safeParse(result.report); + const report = zReport.extend({ + startDate: z.string(), + endDate: z.string(), + }).safeParse(result.report); if (report.success) { return ( diff --git a/apps/start/src/components/chat/chat-report.tsx b/apps/start/src/components/chat/chat-report.tsx index f8538ccd..bc967001 100644 --- a/apps/start/src/components/chat/chat-report.tsx +++ b/apps/start/src/components/chat/chat-report.tsx @@ -1,6 +1,6 @@ import { pushModal } from '@/modals'; import type { - IChartInputAi, + IReport, IChartRange, IChartType, IInterval, @@ -16,7 +16,7 @@ import { Button } from '../ui/button'; export function ChatReport({ lazy, ...props -}: { report: IChartInputAi; lazy: boolean }) { +}: { report: IReport & { startDate: string; endDate: string }; lazy: boolean }) { const [chartType, setChartType] = useState( props.report.chartType, ); diff --git a/apps/start/src/components/overview/overview-top-events.tsx b/apps/start/src/components/overview/overview-top-events.tsx index bd2fe377..6b2fb678 100644 --- a/apps/start/src/components/overview/overview-top-events.tsx +++ b/apps/start/src/components/overview/overview-top-events.tsx @@ -1,7 +1,7 @@ import { useEventQueryFilters } from '@/hooks/use-event-query-filters'; import { useMemo, useState } from 'react'; -import type { IChartInput } from '@openpanel/validation'; +import type { IReportInput } from '@openpanel/validation'; import { useTRPC } from '@/integrations/trpc/react'; import { useQuery } from '@tanstack/react-query'; @@ -74,7 +74,7 @@ export default function OverviewTopEvents({ }, }); - const report: IChartInput = useMemo( + const report: IReportInput = useMemo( () => ({ limit: 1000, projectId, @@ -96,9 +96,7 @@ export default function OverviewTopEvents({ }, ], chartType: 'bar' as const, - lineType: 'monotone' as const, interval, - name: widget.title, range, previous, metric: 'sum' as const, diff --git a/apps/start/src/components/overview/overview-top-geo.tsx b/apps/start/src/components/overview/overview-top-geo.tsx index ab8a8db5..2afaa58a 100644 --- a/apps/start/src/components/overview/overview-top-geo.tsx +++ b/apps/start/src/components/overview/overview-top-geo.tsx @@ -11,6 +11,7 @@ import { useQuery } from '@tanstack/react-query'; import { ChevronRightIcon } from 'lucide-react'; import { ReportChart } from '../report-chart'; import { SerieIcon } from '../report-chart/common/serie-icon'; +import { ReportChartShortcut } from '../report-chart/shortcut'; import { Widget, WidgetBody } from '../widget'; import { OVERVIEW_COLUMNS_NAME } from './overview-constants'; import OverviewDetailsButton from './overview-details-button'; @@ -210,8 +211,8 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
Map
- diff --git a/apps/start/src/components/profiles/profile-charts.tsx b/apps/start/src/components/profiles/profile-charts.tsx index 41797dd9..21428ea6 100644 --- a/apps/start/src/components/profiles/profile-charts.tsx +++ b/apps/start/src/components/profiles/profile-charts.tsx @@ -2,7 +2,7 @@ import { ReportChart } from '@/components/report-chart'; import { Widget, WidgetBody } from '@/components/widget'; import { memo } from 'react'; -import type { IChartProps } from '@openpanel/validation'; +import type { IReport } from '@openpanel/validation'; import { WidgetHead } from '../overview/overview-widget'; type Props = { @@ -12,7 +12,7 @@ type Props = { export const ProfileCharts = memo( ({ profileId, projectId }: Props) => { - const pageViewsChart: IChartProps = { + const pageViewsChart: IReport = { projectId, chartType: 'linear', series: [ @@ -46,7 +46,7 @@ export const ProfileCharts = memo( metric: 'sum', }; - const eventsChart: IChartProps = { + const eventsChart: IReport = { projectId, chartType: 'linear', series: [ diff --git a/apps/start/src/components/report-chart/context.tsx b/apps/start/src/components/report-chart/context.tsx index 8911ab59..fc648623 100644 --- a/apps/start/src/components/report-chart/context.tsx +++ b/apps/start/src/components/report-chart/context.tsx @@ -2,11 +2,7 @@ import isEqual from 'lodash.isequal'; import type { LucideIcon } from 'lucide-react'; import { createContext, useContext, useEffect, useState } from 'react'; -import type { - IChartInput, - IChartProps, - IChartSerie, -} from '@openpanel/validation'; +import type { IChartSerie, IReportInput } from '@openpanel/validation'; export type ReportChartContextType = { options: Partial<{ @@ -27,7 +23,7 @@ export type ReportChartContextType = { onClick: () => void; }[]; }>; - report: IChartInput & { id?: string }; + report: IReportInput & { id?: string }; isLazyLoading: boolean; isEditMode: boolean; shareId?: string; @@ -39,7 +35,7 @@ type ReportChartContextProviderProps = ReportChartContextType & { }; export type ReportChartProps = Partial & { - report: IChartInput; + report: IReportInput & { id?: string }; lazy?: boolean; }; diff --git a/apps/start/src/components/report-chart/funnel/chart.tsx b/apps/start/src/components/report-chart/funnel/chart.tsx index e48fce13..66506f0c 100644 --- a/apps/start/src/components/report-chart/funnel/chart.tsx +++ b/apps/start/src/components/report-chart/funnel/chart.tsx @@ -131,34 +131,36 @@ export function Tables({ series: reportSeries, breakdowns: reportBreakdowns, previous, - funnelWindow, - funnelGroup, + options, }, } = useReportChartContext(); + const funnelOptions = options?.type === 'funnel' ? options : undefined; + const funnelWindow = funnelOptions?.funnelWindow; + const funnelGroup = funnelOptions?.funnelGroup; + const handleInspectStep = (step: (typeof steps)[0], stepIndex: number) => { if (!projectId || !step.event.id) return; // For funnels, we need to pass the step index so the modal can query // users who completed at least that step in the funnel sequence - pushModal('ViewChartUsers', { - type: 'funnel', - report: { - projectId, - series: reportSeries, - breakdowns: reportBreakdowns || [], - interval: interval || 'day', - startDate, - endDate, - range, - previous, - chartType: 'funnel', - metric: 'sum', - funnelWindow, - funnelGroup, - }, - stepIndex, // Pass the step index for funnel queries - }); + pushModal('ViewChartUsers', { + type: 'funnel', + report: { + projectId, + series: reportSeries, + breakdowns: reportBreakdowns || [], + interval: interval || 'day', + startDate, + endDate, + range, + previous, + chartType: 'funnel', + metric: 'sum', + options: funnelOptions, + }, + stepIndex, // Pass the step index for funnel queries + }); }; return (
diff --git a/apps/start/src/components/report-chart/funnel/index.tsx b/apps/start/src/components/report-chart/funnel/index.tsx index 5b519085..9239eb34 100644 --- a/apps/start/src/components/report-chart/funnel/index.tsx +++ b/apps/start/src/components/report-chart/funnel/index.tsx @@ -3,7 +3,7 @@ import type { RouterOutputs } from '@/trpc/client'; import { useQuery } from '@tanstack/react-query'; import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; -import type { IChartInput } from '@openpanel/validation'; +import type { IReportInput } from '@openpanel/validation'; import { AspectContainer } from '../aspect-container'; import { ReportChartEmpty } from '../common/empty'; @@ -19,8 +19,7 @@ export function ReportFunnelChart() { series, range, projectId, - funnelWindow, - funnelGroup, + options, startDate, endDate, previous, @@ -32,23 +31,22 @@ export function ReportFunnelChart() { } = useReportChartContext(); const { range: overviewRange, startDate: overviewStartDate, endDate: overviewEndDate, interval: overviewInterval } = useOverviewOptions(); + const funnelOptions = options?.type === 'funnel' ? options : undefined; + const trpc = useTRPC(); - const input: IChartInput = { + const input: IReportInput = { series, range: overviewRange ?? range, projectId, interval: overviewInterval ?? interval ?? 'day', chartType: 'funnel', breakdowns, - funnelWindow, - funnelGroup, previous, metric: 'sum', startDate: overviewStartDate ?? startDate, endDate: overviewEndDate ?? endDate, limit: 20, - shareId, - reportId: id, + options: funnelOptions, }; const res = useQuery( trpc.chart.funnel.queryOptions(input, { diff --git a/apps/start/src/components/report-chart/retention/index.tsx b/apps/start/src/components/report-chart/retention/index.tsx index e204a4b5..9619ab0e 100644 --- a/apps/start/src/components/report-chart/retention/index.tsx +++ b/apps/start/src/components/report-chart/retention/index.tsx @@ -17,20 +17,29 @@ export function ReportRetentionChart() { series, range, projectId, + options, startDate, endDate, - criteria, interval, }, isLazyLoading, shareId, } = useReportChartContext(); - const { range: overviewRange, startDate: overviewStartDate, endDate: overviewEndDate, interval: overviewInterval } = useOverviewOptions(); + const { + range: overviewRange, + startDate: overviewStartDate, + endDate: overviewEndDate, + interval: overviewInterval, + } = useOverviewOptions(); const eventSeries = series.filter((item) => item.type === 'event'); const firstEvent = (eventSeries[0]?.filters?.[0]?.value ?? []).map(String); const secondEvent = (eventSeries[1]?.filters?.[0]?.value ?? []).map(String); const isEnabled = firstEvent.length > 0 && secondEvent.length > 0 && !isLazyLoading; + + const retentionOptions = options?.type === 'retention' ? options : undefined; + const criteria = retentionOptions?.criteria ?? 'on_or_after'; + const trpc = useTRPC(); const res = useQuery( trpc.chart.cohort.queryOptions( diff --git a/apps/start/src/components/report-chart/sankey/index.tsx b/apps/start/src/components/report-chart/sankey/index.tsx index 9a576197..879d9021 100644 --- a/apps/start/src/components/report-chart/sankey/index.tsx +++ b/apps/start/src/components/report-chart/sankey/index.tsx @@ -1,7 +1,7 @@ import { useTRPC } from '@/integrations/trpc/react'; import { useQuery } from '@tanstack/react-query'; -import type { IChartInput } from '@openpanel/validation'; +import type { IReportInput } from '@openpanel/validation'; import { AspectContainer } from '../aspect-container'; import { ReportChartEmpty } from '../common/empty'; @@ -28,7 +28,7 @@ export function ReportSankeyChart() { return ; } - const input: IChartInput = { + const input: IReportInput = { series, range, projectId, diff --git a/apps/start/src/components/report-chart/shortcut.tsx b/apps/start/src/components/report-chart/shortcut.tsx index c42e6aa4..f53ddf71 100644 --- a/apps/start/src/components/report-chart/shortcut.tsx +++ b/apps/start/src/components/report-chart/shortcut.tsx @@ -26,7 +26,6 @@ export const ReportChartShortcut = ({ return (
@@ -242,7 +240,6 @@ export function ReportItemReadOnly({ )} > ) { + setReport(state, action: PayloadAction) { return { ...state, ...action.payload, @@ -265,7 +261,14 @@ export const reportSlice = createSlice({ changeCriteria(state, action: PayloadAction>) { state.dirty = true; - state.criteria = action.payload; + if (!state.options || state.options.type !== 'retention') { + state.options = { + type: 'retention', + criteria: action.payload, + }; + } else { + state.options.criteria = action.payload; + } }, changeUnit(state, action: PayloadAction) { @@ -275,12 +278,28 @@ export const reportSlice = createSlice({ changeFunnelGroup(state, action: PayloadAction) { state.dirty = true; - state.funnelGroup = action.payload || undefined; + if (!state.options || state.options.type !== 'funnel') { + state.options = { + type: 'funnel', + funnelGroup: action.payload, + funnelWindow: undefined, + }; + } else { + state.options.funnelGroup = action.payload; + } }, changeFunnelWindow(state, action: PayloadAction) { state.dirty = true; - state.funnelWindow = action.payload || undefined; + if (!state.options || state.options.type !== 'funnel') { + state.options = { + type: 'funnel', + funnelGroup: undefined, + funnelWindow: action.payload, + }; + } else { + state.options.funnelWindow = action.payload; + } }, changeOptions(state, action: PayloadAction) { state.dirty = true; diff --git a/apps/start/src/components/report/sidebar/ReportSeries.tsx b/apps/start/src/components/report/sidebar/ReportSeries.tsx index 02c6ae65..e7bd2955 100644 --- a/apps/start/src/components/report/sidebar/ReportSeries.tsx +++ b/apps/start/src/components/report/sidebar/ReportSeries.tsx @@ -332,14 +332,13 @@ export function ReportSeries() { }} placeholder="Select event" items={eventNames} - className="flex-1" /> {showFormula && (