feat(ai): add ai chat to dashboard
This commit is contained in:
@@ -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:^",
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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;
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@openpanel/constants": "workspace:*",
|
||||
"zod": "^3.22.4"
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openpanel/tsconfig": "workspace:*",
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user