diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx index 1f1c899ef7b..ca4705f9733 100644 --- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx +++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/WholeMeetingSummaryResult.tsx @@ -93,7 +93,7 @@ const WholeMeetingSummaryResult = ({meetingRef}: Props) => { diff --git a/packages/client/modules/teamDashboard/components/TeamDashHeader/TeamDashHeader.tsx b/packages/client/modules/teamDashboard/components/TeamDashHeader/TeamDashHeader.tsx index f1c764adfaf..91805c5cf19 100644 --- a/packages/client/modules/teamDashboard/components/TeamDashHeader/TeamDashHeader.tsx +++ b/packages/client/modules/teamDashboard/components/TeamDashHeader/TeamDashHeader.tsx @@ -95,10 +95,14 @@ const TeamDashHeader = (props: Props) => { ...DashboardAvatars_team id name + hasInsightsFlag: featureFlag(featureName: "insights") organization { id name } + viewerTeamMember { + isLead + } teamMembers(sortBy: "preferredName") { ...InviteTeamMemberAvatar_teamMembers ...DashboardAvatar_teamMember @@ -109,15 +113,23 @@ const TeamDashHeader = (props: Props) => { `, teamRef ) - const {organization, id: teamId, name: teamName, teamMembers} = team + const { + organization, + id: teamId, + name: teamName, + teamMembers, + viewerTeamMember, + hasInsightsFlag + } = team const {name: orgName, id: orgId} = organization + const canViewInsights = viewerTeamMember?.isLead && hasInsightsFlag const {history} = useRouter() const tabs = [ {label: 'Activity', path: 'activity'}, {label: 'Tasks', path: 'tasks'}, {label: 'Integrations', path: 'integrations'}, - {label: 'Insights', path: 'insights'} + ...(canViewInsights ? [{label: 'Insights', path: 'insights'}] : []) ] const activePath = location.pathname.split('/').pop() diff --git a/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightContent.tsx b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightContent.tsx new file mode 100644 index 00000000000..849181d07e6 --- /dev/null +++ b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightContent.tsx @@ -0,0 +1,121 @@ +import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome' +import graphql from 'babel-plugin-relay/macro' +import dayjs from 'dayjs' +import {marked} from 'marked' +import React from 'react' +import {useFragment} from 'react-relay' +import sanitizeHtml from 'sanitize-html' +import {TeamInsightContent_team$key} from '../../../../__generated__/TeamInsightContent_team.graphql' + +interface Props { + teamName: string + insightRef: TeamInsightContent_team$key +} + +const TeamInsightContent = (props: Props) => { + const {insightRef, teamName} = props + const insight = useFragment( + graphql` + fragment TeamInsightContent_team on Insight { + meetingsCount + wins + challenges + startDateTime + endDateTime + } + `, + insightRef + ) + const {meetingsCount, wins, challenges} = insight + + const renderMarkdown = (text: string) => { + const renderedText = marked(text, { + gfm: true, + breaks: true + }) as string + return sanitizeHtml(renderedText, { + allowedTags: sanitizeHtml.defaults.allowedTags.concat(['a']), + allowedAttributes: { + ...sanitizeHtml.defaults.allowedAttributes, + a: ['href', 'target', 'rel'] + }, + transformTags: { + a: (tagName, attribs) => { + return { + tagName, + attribs: { + ...attribs, + target: '_blank', + rel: 'noopener noreferrer' + } + } + } + } + }) + } + + const formatDateRange = (start: string, end: string) => { + const startDate = dayjs(start) + const endDate = dayjs(end) + + if (startDate.year() === endDate.year()) { + return `${startDate.format('MMM')} to ${endDate.format('MMM YYYY')}` + } else { + return `${startDate.format('MMM YYYY')} to ${endDate.format('MMM YYYY')}` + } + } + + const dateRange = insight + ? formatDateRange(insight.startDateTime, insight.endDateTime) + : 'Date range not available' + + return ( +
+

+ + Insights - {dateRange} +

+

Summarized {meetingsCount} meetings

+ + {wins && wins.length > 0 && ( + <> +

Wins

+

+ What wins has "{teamName}" seen during this timeframe? +

+ + + )} + + {challenges && challenges.length > 0 && ( + <> +

Challenges

+

+ What challenges has "{teamName}" faced during this timeframe? +

+ + + )} +
+ ) +} + +export default TeamInsightContent diff --git a/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightEmptyState.tsx b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightEmptyState.tsx new file mode 100644 index 00000000000..9a4c13487e3 --- /dev/null +++ b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsightEmptyState.tsx @@ -0,0 +1,78 @@ +import AddIcon from '@mui/icons-material/Add' +import React from 'react' +import insightsEmptyStateImg from '../../../../../../static/images/illustrations/insights-empty-state.png' +import useAtmosphere from '../../../../hooks/useAtmosphere' +import useMutationProps from '../../../../hooks/useMutationProps' +import GenerateInsightMutation from '../../../../mutations/GenerateInsightMutation' +import plural from '../../../../utils/plural' + +interface Props { + meetingsCount?: number + teamId?: string +} + +const TeamInsightEmptyState = (props: Props) => { + const {meetingsCount, teamId} = props + const atmosphere = useAtmosphere() + const {onError, error, onCompleted, submitMutation, submitting} = useMutationProps() + + if (meetingsCount === undefined || !teamId) return null + + const canGenerateInsight = meetingsCount > 1 + + const handleGenerateInsight = () => { + if (submitting) return + submitMutation() + const now = new Date() + const threeMonthsAgo = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate()) + GenerateInsightMutation( + atmosphere, + { + teamId, + startDate: threeMonthsAgo.toISOString(), // TODO: let users choose date range + endDate: now.toISOString() + }, + {onError, onCompleted} + ) + } + + return ( +
+ Empty state +
+

+ Your team has completed {meetingsCount}{' '} + {plural(meetingsCount, 'meeting')}. +

+ {canGenerateInsight ? ( +

+ This should be{' '} + + good fodder + {' '} + for some interesting insights! +

+ ) : ( +

+ Create more meetings to start generating insights for your team. +

+ )} +
+ {canGenerateInsight && ( + + )} + {error && ( +
{error.message}
+ )} +
+ ) +} + +export default TeamInsightEmptyState diff --git a/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsights.tsx b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsights.tsx index b10f1eb9238..9232897bdd8 100644 --- a/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsights.tsx +++ b/packages/client/modules/teamDashboard/components/TeamDashInsightsTab/TeamInsights.tsx @@ -1,8 +1,9 @@ -import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome' import graphql from 'babel-plugin-relay/macro' import React from 'react' import {PreloadedQuery, usePreloadedQuery} from 'react-relay' import {TeamInsightsQuery} from '../../../../__generated__/TeamInsightsQuery.graphql' +import TeamInsightContent from './TeamInsightContent' +import TeamInsightEmptyState from './TeamInsightEmptyState' interface Props { queryRef: PreloadedQuery @@ -12,36 +13,52 @@ const query = graphql` query TeamInsightsQuery($teamId: ID!) { viewer { team(teamId: $teamId) { - id + ...TeamInsights_team @relay(mask: false) name + insight { + wins + ...TeamInsightContent_team + } } } } ` -const Insights = (props: Props) => { +graphql` + fragment TeamInsights_team on Team { + id + retroMeetingsCount + } +` + +const TeamInsights = (props: Props) => { const {queryRef} = props const data = usePreloadedQuery(query, queryRef) - // TODO: use the query rather than just console logging it - console.log('🚀 ~ data:', data) + const {viewer} = data + const {team} = viewer + const {id: teamId, insight, name, retroMeetingsCount} = team ?? {} return (

Only you (as Team Lead) can see Team Insights. Insights are auto-generated.{' '} - + Give us feedback

-
-

- - Insights - Aug to Sep 2024 -

-
+ {insight ? ( + + ) : ( + + )}
) } -export default Insights +export default TeamInsights diff --git a/packages/client/mutations/EndRetrospectiveMutation.ts b/packages/client/mutations/EndRetrospectiveMutation.ts index c7f3d2c1df2..e2c37a4ec49 100644 --- a/packages/client/mutations/EndRetrospectiveMutation.ts +++ b/packages/client/mutations/EndRetrospectiveMutation.ts @@ -52,6 +52,7 @@ graphql` activeMeetings { id } + ...TeamInsights_team insights { ...TeamDashInsights_insights } diff --git a/packages/client/mutations/GenerateInsightMutation.ts b/packages/client/mutations/GenerateInsightMutation.ts index d7bbe485b4f..b0324478e70 100644 --- a/packages/client/mutations/GenerateInsightMutation.ts +++ b/packages/client/mutations/GenerateInsightMutation.ts @@ -5,8 +5,14 @@ import {StandardMutation} from '../types/relayMutations' graphql` fragment GenerateInsightMutation_team on GenerateInsightSuccess { - wins - challenges + team { + id + insight { + wins + challenges + meetingsCount + } + } } ` diff --git a/packages/client/shared/gqlIds/InsightId.ts b/packages/client/shared/gqlIds/InsightId.ts new file mode 100644 index 00000000000..ebe082ca92c --- /dev/null +++ b/packages/client/shared/gqlIds/InsightId.ts @@ -0,0 +1,7 @@ +export const InsightId = { + join: (ownerId: string, databaseId: number) => `insight:${ownerId}:${databaseId}`, + split: (id: string) => { + const [, ownerId, databaseId] = id.split(':') + return {ownerId, databaseId} + } +} diff --git a/packages/client/styles/theme/global.css b/packages/client/styles/theme/global.css index b1a76203c76..4b8d81dbb31 100644 --- a/packages/client/styles/theme/global.css +++ b/packages/client/styles/theme/global.css @@ -188,7 +188,7 @@ @apply m-0 rounded-[1px] border-l-2 border-solid border-l-slate-500 bg-slate-200 py-0 px-[8px] font-mono font-[13px] leading-normal; } -.summary-link-style a { +.link-style a { @apply text-sky-500; text-decoration: underline; } diff --git a/packages/client/subscriptions/TeamSubscription.ts b/packages/client/subscriptions/TeamSubscription.ts index 17527fdee9b..3e7b524d4ab 100644 --- a/packages/client/subscriptions/TeamSubscription.ts +++ b/packages/client/subscriptions/TeamSubscription.ts @@ -46,6 +46,9 @@ const subscription = graphql` subscription TeamSubscription { teamSubscription { fieldName + GenerateInsightSuccess { + ...GenerateInsightMutation_team @relay(mask: false) + } UpdateRecurrenceSettingsSuccess { ...UpdateRecurrenceSettingsMutation_team @relay(mask: false) } diff --git a/packages/server/dataloader/customLoaderMakers.ts b/packages/server/dataloader/customLoaderMakers.ts index dd75986ec34..86cabe39472 100644 --- a/packages/server/dataloader/customLoaderMakers.ts +++ b/packages/server/dataloader/customLoaderMakers.ts @@ -25,7 +25,7 @@ import getMeetingTaskEstimates, { MeetingTaskEstimatesResult } from '../postgres/queries/getMeetingTaskEstimates' import {selectMeetingSettings, selectNewMeetings, selectTeams} from '../postgres/select' -import {MeetingSettings, OrganizationUser, Team} from '../postgres/types' +import {Insight, MeetingSettings, OrganizationUser, Team} from '../postgres/types' import {AnyMeeting, MeetingTypeEnum} from '../postgres/types/Meeting' import {Logger} from '../utils/Logger' import getRedis from '../utils/getRedis' @@ -832,6 +832,25 @@ export const meetingCount = (parent: RootDataLoader, dependsOn: RegisterDependsO ) } +export const latestInsightByTeamId = (parent: RootDataLoader) => { + return new NullableDataLoader( + async (teamIds) => { + const pg = getKysely() + const insights = await pg + .selectFrom('Insight') + .where('teamId', 'in', teamIds) + .selectAll() + .orderBy('createdAt', 'desc') + .execute() + + return teamIds.map((teamId) => insights.find((insight) => insight.teamId === teamId) || null) + }, + { + ...parent.dataLoaderOptions + } + ) +} + export const featureFlagByOwnerId = (parent: RootDataLoader) => { return new DataLoader<{ownerId: string; featureName: string}, boolean, string>( async (keys) => { diff --git a/packages/server/graphql/public/mutations/generateInsight.ts b/packages/server/graphql/public/mutations/generateInsight.ts index c7ec6cbb459..d463384f32d 100644 --- a/packages/server/graphql/public/mutations/generateInsight.ts +++ b/packages/server/graphql/public/mutations/generateInsight.ts @@ -1,4 +1,8 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import toTeamMemberId from '../../../../client/utils/relay/toTeamMemberId' import getKysely from '../../../postgres/getKysely' +import {getUserId} from '../../../utils/authorization' +import publish from '../../../utils/publish' import standardError from '../../../utils/standardError' import {MutationResolvers} from '../resolverTypes' import {getSummaries} from './helpers/getSummaries' @@ -7,8 +11,25 @@ import {getTopics} from './helpers/getTopics' const generateInsight: MutationResolvers['generateInsight'] = async ( _source, {teamId, startDate, endDate, useSummaries = true, prompt}, - {dataLoader} + context ) => { + const {dataLoader, socketId: mutatorId, authToken} = context + const viewerId = getUserId(authToken) + const teamMemberId = toTeamMemberId(teamId, viewerId) + const teamMember = await dataLoader.get('teamMembers').loadNonNull(teamMemberId) + const isLead = teamMember.isLead + if (!isLead) { + return standardError(new Error('Only team leads can generate insights'), {userId: viewerId}) + } + const hasInsightsFlag = await dataLoader + .get('featureFlagByOwnerId') + .load({ownerId: teamId, featureName: 'insights'}) + if (!hasInsightsFlag) { + return standardError(new Error('Insights are not enabled for this team'), {userId: viewerId}) + } + + const operationId = dataLoader.share() + const subOptions = {operationId, mutatorId} if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) { return standardError( new Error('Invalid date format. Please use ISO 8601 format (e.g., 2024-01-01T00:00:00Z).') @@ -26,21 +47,32 @@ const generateInsight: MutationResolvers['generateInsight'] = async ( if ('error' in response) { return response } - const {wins, challenges} = response + const {wins, challenges, meetingIds} = response const pg = getKysely() - await pg + const [insertedInsight] = await pg .insertInto('Insight') .values({ teamId, wins, challenges, + meetingsCount: meetingIds.length, startDateTime: startDate, endDateTime: endDate }) + .returning(['id']) .execute() - return response + if (!insertedInsight) { + return standardError(new Error('Failed to insert insight')) + } + const data = { + teamId + } + + publish(SubscriptionChannel.TEAM, teamId, 'GenerateInsightSuccess', data, subOptions) + + return data } export default generateInsight diff --git a/packages/server/graphql/public/typeDefs/GenerateInsightPayload.graphql b/packages/server/graphql/public/typeDefs/GenerateInsightPayload.graphql new file mode 100644 index 00000000000..13629c3d050 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/GenerateInsightPayload.graphql @@ -0,0 +1,4 @@ +""" +Return value for generateInsight, which could be an error +""" +union GenerateInsightPayload = ErrorPayload | GenerateInsightSuccess diff --git a/packages/server/graphql/public/typeDefs/GenerateInsightSuccess.graphql b/packages/server/graphql/public/typeDefs/GenerateInsightSuccess.graphql new file mode 100644 index 00000000000..b40ebf68e2d --- /dev/null +++ b/packages/server/graphql/public/typeDefs/GenerateInsightSuccess.graphql @@ -0,0 +1,3 @@ +type GenerateInsightSuccess { + team: Team! +} diff --git a/packages/server/graphql/public/typeDefs/Insight.graphql b/packages/server/graphql/public/typeDefs/Insight.graphql new file mode 100644 index 00000000000..3f5e4d55f33 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/Insight.graphql @@ -0,0 +1,39 @@ +type Insight { + id: ID! + teamId: ID! + + """ + Start date and time of the insight period + """ + startDateTime: DateTime! + + """ + End date and time of the insight period + """ + endDateTime: DateTime! + + """ + List of AI generated wins + """ + wins: [String!]! + + """ + List of AI generated challenges + """ + challenges: [String!]! + + """ + The count of the meetings used to generate the insight + """ + meetingsCount: Int! + + """ + Timestamp when the insight was created + """ + createdAt: DateTime! + + """ + Timestamp when the insight was last updated + """ + updatedAt: DateTime! +} diff --git a/packages/server/graphql/public/typeDefs/Mutation.graphql b/packages/server/graphql/public/typeDefs/Mutation.graphql index b71bc91fe3c..a793fc21632 100644 --- a/packages/server/graphql/public/typeDefs/Mutation.graphql +++ b/packages/server/graphql/public/typeDefs/Mutation.graphql @@ -422,6 +422,17 @@ type Mutation { invitees: [Email!]! ): InviteToTeamPayload! + """ + Generate an insight for a team + """ + generateInsight( + teamId: ID! + startDate: DateTime! + endDate: DateTime! + useSummaries: Boolean + prompt: String + ): GenerateInsightPayload! + """ Move a template dimension """ diff --git a/packages/server/graphql/public/typeDefs/Team.graphql b/packages/server/graphql/public/typeDefs/Team.graphql index 5bed3eae97e..dccbb9e7eb9 100644 --- a/packages/server/graphql/public/typeDefs/Team.graphql +++ b/packages/server/graphql/public/typeDefs/Team.graphql @@ -128,6 +128,11 @@ type Team { ): NewMeeting organization: Organization! + """ + The AI insight for the team. Null if not enough data or feature flag isn't set + """ + insight: Insight + """ The agenda items for the upcoming or current meeting """ @@ -191,4 +196,9 @@ type Team { The team member that is the team lead """ teamLead: TeamMember! + + """ + The number of retro meetings the team has had + """ + retroMeetingsCount: Int! } diff --git a/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql b/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql index 98cafd8758b..5d988e06788 100644 --- a/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql +++ b/packages/server/graphql/public/typeDefs/TeamSubscriptionPayload.graphql @@ -71,4 +71,5 @@ type TeamSubscriptionPayload { UpdateRecurrenceSettingsSuccess: UpdateRecurrenceSettingsSuccess UpdateDimensionFieldSuccess: UpdateDimensionFieldSuccess UpdateTemplateCategorySuccess: UpdateTemplateCategorySuccess + GenerateInsightSuccess: GenerateInsightSuccess } diff --git a/packages/server/graphql/public/typeDefs/generateInsight.graphql b/packages/server/graphql/public/typeDefs/generateInsight.graphql deleted file mode 100644 index d07b656a8f0..00000000000 --- a/packages/server/graphql/public/typeDefs/generateInsight.graphql +++ /dev/null @@ -1,34 +0,0 @@ -extend type Mutation { - """ - Generate an insight for a team - """ - generateInsight( - teamId: ID! - startDate: DateTime! - endDate: DateTime! - useSummaries: Boolean - prompt: String - ): GenerateInsightPayload! -} - -""" -Return value for generateInsight, which could be an error -""" -union GenerateInsightPayload = ErrorPayload | GenerateInsightSuccess - -type GenerateInsightSuccess { - """ - The insights generated focusing on the wins of the team - """ - wins: [String!]! - - """ - The insights generated focusing on the challenges team are facing - """ - challenges: [String!]! - - """ - The meetings that were used to generate the insights - """ - meetings: [RetrospectiveMeeting!]! -} diff --git a/packages/server/graphql/public/types/GenerateInsightSuccess.ts b/packages/server/graphql/public/types/GenerateInsightSuccess.ts index a22c5a7e1cd..53a3ddd4200 100644 --- a/packages/server/graphql/public/types/GenerateInsightSuccess.ts +++ b/packages/server/graphql/public/types/GenerateInsightSuccess.ts @@ -1,18 +1,12 @@ -import isValid from '../../isValid' import {GenerateInsightSuccessResolvers} from '../resolverTypes' export type GenerateInsightSuccessSource = { - wins: string[] - challenges: string[] - meetingIds: string[] + teamId: string } const GenerateInsightSuccess: GenerateInsightSuccessResolvers = { - wins: ({wins}) => wins, - challenges: ({challenges}) => challenges, - meetings: async ({meetingIds}, _args, {dataLoader}) => { - const meetings = await dataLoader.get('newMeetings').loadMany(meetingIds) - return meetings.filter(isValid).filter((m) => m.meetingType === 'retrospective') + team: async ({teamId}, _args, {dataLoader}) => { + return await dataLoader.get('teams').loadNonNull(teamId) } } diff --git a/packages/server/graphql/public/types/Team.ts b/packages/server/graphql/public/types/Team.ts index 8cd5ccbbae9..1c3975fd4dc 100644 --- a/packages/server/graphql/public/types/Team.ts +++ b/packages/server/graphql/public/types/Team.ts @@ -1,4 +1,5 @@ import TeamInsightsId from 'parabol-client/shared/gqlIds/TeamInsightsId' +import {InsightId} from '../../../../client/shared/gqlIds/InsightId' import toTeamMemberId from '../../../../client/utils/relay/toTeamMemberId' import {getUserId, isTeamMember} from '../../../utils/authorization' import {getFeatureTier} from '../../types/helpers/getFeatureTier' @@ -55,6 +56,19 @@ const Team: TeamResolvers = { const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId) return teamMembers.find((teamMember) => teamMember.isLead)! }, + retroMeetingsCount: async ({id: teamId}, _args, {dataLoader}) => { + const meetings = await dataLoader.get('completedMeetingsByTeamId').load(teamId) + const retroMeetings = meetings.filter((meeting) => meeting.meetingType === 'retrospective') + return retroMeetings.length + }, + insight: async ({id: teamId}, _args, {dataLoader}) => { + const insight = await dataLoader.get('latestInsightByTeamId').load(teamId) + if (!insight) return null + return { + ...insight, + id: InsightId.join(teamId, insight.id) + } + }, featureFlag: async ({id: teamId}, {featureName}, {dataLoader}) => { return await dataLoader.get('featureFlagByOwnerId').load({ownerId: teamId, featureName}) } diff --git a/packages/server/postgres/migrations/1728596433080_addMeetingsCountToInsight.ts b/packages/server/postgres/migrations/1728596433080_addMeetingsCountToInsight.ts new file mode 100644 index 00000000000..015d54f428a --- /dev/null +++ b/packages/server/postgres/migrations/1728596433080_addMeetingsCountToInsight.ts @@ -0,0 +1,22 @@ +import {Client} from 'pg' +import getPgConfig from '../getPgConfig' + +export async function up() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + ALTER TABLE "Insight" + ADD COLUMN "meetingsCount" INTEGER NOT NULL DEFAULT 0; + `) + await client.end() +} + +export async function down() { + const client = new Client(getPgConfig()) + await client.connect() + await client.query(` + ALTER TABLE "Insight" + DROP COLUMN IF EXISTS "meetingsCount"; + `) + await client.end() +} diff --git a/packages/server/postgres/types/index.d.ts b/packages/server/postgres/types/index.d.ts index 587e2372564..11efe61389e 100644 --- a/packages/server/postgres/types/index.d.ts +++ b/packages/server/postgres/types/index.d.ts @@ -1,6 +1,7 @@ import {SelectQueryBuilder, Selectable} from 'kysely' import { Discussion as DiscussionPG, + Insight as InsightPG, OrganizationUser as OrganizationUserPG, TeamMember as TeamMemberPG } from '../pg.d' @@ -72,6 +73,7 @@ export type SlackNotification = ExtractTypeFromQueryBuilderSelect export type ReflectPrompt = ExtractTypeFromQueryBuilderSelect +export type Insight = Selectable export type NewMeeting = ExtractTypeFromQueryBuilderSelect export type NewFeature = ExtractTypeFromQueryBuilderSelect diff --git a/static/images/illustrations/insights-empty-state.png b/static/images/illustrations/insights-empty-state.png new file mode 100644 index 00000000000..f4f445c4fd5 Binary files /dev/null and b/static/images/illustrations/insights-empty-state.png differ