feat(ai): add ai chat to dashboard

This commit is contained in:
Carl-Gerhard Lindesvärd
2025-04-15 14:30:21 +02:00
parent 804a9c8056
commit 34769a5d58
46 changed files with 2624 additions and 1449 deletions

View File

@@ -17,7 +17,7 @@
"p-limit": "^6.1.0",
"progress": "^2.0.3",
"ramda": "^0.29.1",
"zod": "^3.22.4"
"zod": "catalog:"
},
"devDependencies": {
"@openpanel/db": "workspace:^",

View File

@@ -29,7 +29,7 @@
"sqlstring": "^2.3.3",
"superjson": "^1.13.3",
"uuid": "^9.0.1",
"zod": "^3.22.4"
"zod": "catalog:"
},
"devDependencies": {
"@openpanel/tsconfig": "workspace:*",

View File

@@ -0,0 +1,13 @@
-- CreateTable
CREATE TABLE "chats" (
"id" TEXT NOT NULL DEFAULT gen_random_uuid(),
"messages" JSONB NOT NULL,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "chats_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "chats" ADD CONSTRAINT "chats_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -30,6 +30,17 @@ enum ProjectType {
backend
}
model Chat {
id String @id @default(dbgenerated("gen_random_uuid()"))
messages Json
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@map("chats")
}
model Organization {
id String @id @default(dbgenerated("gen_random_uuid()"))
name String
@@ -184,6 +195,7 @@ model Project {
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
Chat Chat[]
@@map("projects")
}

View File

@@ -12,7 +12,7 @@
"react-dom": "18.2.0",
"resend": "^4.0.1",
"responsive-react-email": "^0.0.5",
"zod": "^3.22.4"
"zod": "catalog:"
},
"devDependencies": {
"@openpanel/tsconfig": "workspace:*",

View File

@@ -28,7 +28,7 @@
"sqlstring": "^2.3.3",
"superjson": "^1.13.3",
"uuid": "^9.0.1",
"zod": "^3.22.4"
"zod": "catalog:"
},
"devDependencies": {
"@openpanel/tsconfig": "workspace:*",

View File

@@ -28,7 +28,12 @@ import {
} from 'date-fns';
import { getProjectAccessCached } from '../access';
import { TRPCAccessError } from '../errors';
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
import {
cacheMiddleware,
createTRPCRouter,
protectedProcedure,
publicProcedure,
} from '../trpc';
import {
getChart,
getChartPrevStartEndDate,
@@ -42,6 +47,8 @@ function utc(date: string | Date) {
return formatISO(date).replace('T', ' ').slice(0, 19);
}
const cacher = cacheMiddleware(60);
export const chartRouter = createTRPCRouter({
events: protectedProcedure
.input(
@@ -220,13 +227,27 @@ export const chartRouter = createTRPCRouter({
};
}),
chart: publicProcedure.input(zChartInput).query(async ({ input, ctx }) => {
if (ctx.session.userId) {
const access = await getProjectAccessCached({
projectId: input.projectId,
userId: ctx.session.userId,
});
if (!access) {
chart: publicProcedure
.use(cacher)
.input(zChartInput)
.query(async ({ input, ctx }) => {
if (ctx.session.userId) {
const access = await getProjectAccessCached({
projectId: input.projectId,
userId: ctx.session.userId,
});
if (!access) {
const share = await db.shareOverview.findFirst({
where: {
projectId: input.projectId,
},
});
if (!share) {
throw TRPCAccessError('You do not have access to this project');
}
}
} else {
const share = await db.shareOverview.findFirst({
where: {
projectId: input.projectId,
@@ -237,20 +258,9 @@ export const chartRouter = createTRPCRouter({
throw TRPCAccessError('You do not have access to this project');
}
}
} else {
const share = await db.shareOverview.findFirst({
where: {
projectId: input.projectId,
},
});
if (!share) {
throw TRPCAccessError('You do not have access to this project');
}
}
return getChart(input);
}),
return getChart(input);
}),
cohort: protectedProcedure
.input(
z.object({

View File

@@ -7,7 +7,7 @@
},
"dependencies": {
"@openpanel/constants": "workspace:*",
"zod": "^3.22.4"
"zod": "catalog:"
},
"devDependencies": {
"@openpanel/tsconfig": "workspace:*",

View File

@@ -19,27 +19,48 @@ export function objectToZodEnums<K extends string>(
export const mapKeys = objectToZodEnums;
export const zChartEventFilter = z.object({
id: z.string().optional(),
name: z.string(),
operator: z.enum(objectToZodEnums(operators)),
value: z.array(z.string().or(z.number()).or(z.boolean()).or(z.null())),
id: z.string().optional().describe('Unique identifier for the filter'),
name: z.string().describe('The property name to filter on'),
operator: z
.enum(objectToZodEnums(operators))
.describe('The operator to use for the filter'),
value: z
.array(z.string().or(z.number()).or(z.boolean()).or(z.null()))
.describe('The values to filter on'),
});
export const zChartEvent = z.object({
id: z.string().optional(),
name: z.string(),
displayName: z.string().optional(),
property: z.string().optional(),
segment: z.enum([
'event',
'user',
'session',
'user_average',
'one_event_per_user',
'property_sum',
'property_average',
]),
filters: z.array(zChartEventFilter).default([]),
id: z
.string()
.optional()
.describe('Unique identifier for the chart event configuration'),
name: z.string().describe('The name of the event as tracked in the system'),
displayName: z
.string()
.optional()
.describe('A user-friendly name for display purposes'),
property: z
.string()
.optional()
.describe(
'Optional property of the event used for specific segment calculations (e.g., value for property_sum/average)',
),
segment: z
.enum([
'event',
'user',
'session',
'user_average',
'one_event_per_user',
'property_sum',
'property_average',
])
.default('event')
.describe('Defines how the event data should be segmented or aggregated'),
filters: z
.array(zChartEventFilter)
.default([])
.describe('Filters applied specifically to this event'),
});
export const zChartBreakdown = z.object({
id: z.string().optional(),
@@ -62,30 +83,95 @@ export const zRange = z.enum(objectToZodEnums(timeWindows));
export const zCriteria = z.enum(['on_or_after', 'on']);
export const zChartInput = z.object({
chartType: zChartType.default('linear'),
interval: zTimeInterval.default('day'),
events: zChartEvents,
breakdowns: zChartBreakdowns.default([]),
range: zRange.default('30d'),
previous: z.boolean().default(false),
formula: z.string().optional(),
metric: zMetric.default('sum'),
projectId: z.string(),
startDate: z.string().nullish(),
endDate: z.string().nullish(),
limit: z.number().optional(),
offset: z.number().optional(),
criteria: zCriteria.optional(),
funnelGroup: z.string().optional(),
funnelWindow: z.number().optional(),
chartType: zChartType
.default('linear')
.describe('What type of chart should be displayed'),
interval: zTimeInterval
.default('day')
.describe(
'The time interval for data aggregation (e.g., day, week, month)',
),
events: zChartEvents.describe(
'Array of events to be tracked and displayed in the chart',
),
breakdowns: zChartBreakdowns
.default([])
.describe('Array of dimensions to break down the data by'),
range: zRange
.default('30d')
.describe('The time range for which data should be displayed'),
previous: z
.boolean()
.default(false)
.describe('Whether to show data from the previous period for comparison'),
formula: z
.string()
.optional()
.describe('Custom formula for calculating derived metrics'),
metric: zMetric
.default('sum')
.describe(
'The aggregation method for the metric (e.g., sum, count, average)',
),
projectId: z.string().describe('The ID of the project this chart belongs to'),
startDate: z
.string()
.nullish()
.describe(
'Custom start date for the data range (overrides range if provided)',
),
endDate: z
.string()
.nullish()
.describe(
'Custom end date for the data range (overrides range if provided)',
),
limit: z
.number()
.optional()
.describe('Limit how many series should be returned'),
offset: z
.number()
.optional()
.describe('Skip how many series should be returned'),
criteria: zCriteria
.optional()
.describe('Filtering criteria for retention chart (e.g., on_or_after, on)'),
funnelGroup: z
.string()
.optional()
.describe(
'Group identifier for funnel analysis, e.g. "profile_id" or "session_id"',
),
funnelWindow: z
.number()
.optional()
.describe('Time window in hours for funnel analysis'),
});
export const zReportInput = zChartInput.extend({
name: z.string(),
lineType: zLineType,
unit: z.string().optional(),
name: z.string().describe('The user-defined name for the report'),
lineType: zLineType.describe('The visual style of the line in the chart'),
unit: z
.string()
.optional()
.describe(
"Optional unit of measurement for the chart's Y-axis (e.g., $, %, users)",
),
});
export const zChartInputAI = zReportInput
.omit({
startDate: true,
endDate: true,
lineType: true,
unit: true,
})
.extend({
startDate: z.string().describe('The start date for the report'),
endDate: z.string().describe('The end date for the report'),
});
export const zInviteUser = z.object({
email: z.string().email(),
organizationId: z.string(),

View File

@@ -4,6 +4,7 @@ import type {
zChartBreakdown,
zChartEvent,
zChartInput,
zChartInputAI,
zChartType,
zCriteria,
zLineType,
@@ -14,6 +15,7 @@ import type {
} from './index';
export type IChartInput = z.infer<typeof zChartInput>;
export type IChartInputAi = z.infer<typeof zChartInputAI>;
export type IChartProps = z.infer<typeof zReportInput> & {
name: string;
lineType: IChartLineType;