From f236a8a467becf929bcb1936b05c978682e6f7b2 Mon Sep 17 00:00:00 2001 From: Bartosz Jarocki Date: Tue, 5 Dec 2023 21:14:30 +0100 Subject: [PATCH 1/6] feat: added option to modify icebreakers with ai --- .../NewCheckInQuestion.tsx | 225 +++++++++++++----- .../ModifyCheckInQuestionMutation.ts | 46 ++++ .../typeDefs/updateOrgFeatureFlag.graphql | 1 + .../public/mutations/modifyCheckInQuestion.ts | 46 ++++ .../public/typeDefs/Organization.graphql | 1 + .../typeDefs/modifyCheckInQuestion.graphql | 25 ++ .../public/types/OrganizationFeatureFlags.ts | 3 +- packages/server/utils/OpenAIServerManager.ts | 39 +++ 8 files changed, 328 insertions(+), 58 deletions(-) create mode 100644 packages/client/mutations/ModifyCheckInQuestionMutation.ts create mode 100644 packages/server/graphql/public/mutations/modifyCheckInQuestion.ts create mode 100644 packages/server/graphql/public/typeDefs/modifyCheckInQuestion.graphql diff --git a/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx b/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx index a4b795672a2..4f467dfcd2f 100644 --- a/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx +++ b/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled' import {Create as CreateIcon, Refresh as RefreshIcon} from '@mui/icons-material' import graphql from 'babel-plugin-relay/macro' -import {convertToRaw, EditorState, SelectionState} from 'draft-js' +import {ContentState, convertToRaw, EditorState, SelectionState} from 'draft-js' import React, {useRef, useState} from 'react' import {useFragment} from 'react-relay' import {NewCheckInQuestion_meeting$key} from '~/__generated__/NewCheckInQuestion_meeting.graphql' @@ -13,8 +13,15 @@ import {MenuPosition} from '../../../../hooks/useCoords' import useEditorState from '../../../../hooks/useEditorState' import useTooltip from '../../../../hooks/useTooltip' import UpdateNewCheckInQuestionMutation from '../../../../mutations/UpdateNewCheckInQuestionMutation' +import ModifyCheckInQuestionMutation from '../../../../mutations/ModifyCheckInQuestionMutation' import {PALETTE} from '../../../../styles/paletteV3' import convertToTaskContent from '../../../../utils/draftjs/convertToTaskContent' +import useMutationProps from '../../../../hooks/useMutationProps' +import clsx from 'clsx' +import { + ModifyCheckInQuestionMutation$data, + ModifyType +} from '../../../../__generated__/ModifyCheckInQuestionMutation.graphql' const CogIcon = styled('div')({ color: PALETTE.SLATE_700, @@ -77,12 +84,27 @@ const NewCheckInQuestion = (props: Props) => { checkInQuestion } } + team { + organization { + featureFlags { + aiIcebreakers + } + } + } } `, meetingRef ) const [isEditing, setIsEditing] = useState(false) - const {id: meetingId, localPhase, facilitatorUserId} = meeting + const [aiUpdatedIcebreaker, setAiUpdatedIcebreaker] = useState('') + const { + id: meetingId, + localPhase, + facilitatorUserId, + team: { + organization: {featureFlags} + } + } = meeting const {checkInQuestion} = localPhase const [editorState, setEditorState] = useEditorState(checkInQuestion) const updateQuestion = (nextEditorState: EditorState) => { @@ -94,6 +116,7 @@ const NewCheckInQuestion = (props: Props) => { const nextCheckInQuestion = nextContent.hasText() ? JSON.stringify(convertToRaw(nextContent)) : '' + if (nextCheckInQuestion === checkInQuestion) return UpdateNewCheckInQuestionMutation(atmosphere, { meetingId, @@ -115,7 +138,7 @@ const NewCheckInQuestion = (props: Props) => { } const focusQuestion = () => { - closeEditIcebreakerTooltip() + closeTooltip() editorRef.current && editorRef.current.focus() const selection = editorState.getSelection() const contentState = editorState.getCurrentContent() @@ -128,71 +151,159 @@ const NewCheckInQuestion = (props: Props) => { } const {viewerId} = atmosphere const isFacilitating = facilitatorUserId === viewerId + const tip = 'Tap to customize the Icebreaker' // eslint-disable-next-line react-hooks/rules-of-hooks + const {tooltipPortal, openTooltip, closeTooltip, originRef} = useTooltip( + MenuPosition.UPPER_CENTER, + { + delay: 300, + disabled: isEditing || !isFacilitating + } + ) + const {submitting, submitMutation} = useMutationProps() const { - tooltipPortal: editIcebreakerTooltipPortal, - openTooltip: openEditIcebreakerTooltip, - closeTooltip: closeEditIcebreakerTooltip, - originRef: editIcebreakerOriginRef - } = useTooltip(MenuPosition.UPPER_CENTER, { - disabled: isEditing || !isFacilitating - }) - const { - tooltipPortal: refreshIcebreakerTooltipPortal, - openTooltip: openRefreshIcebreakerTooltip, - closeTooltip: closeRefreshIcebreakerTooltip, - originRef: refreshIcebreakerOriginRef - } = useTooltip(MenuPosition.UPPER_CENTER, { - disabled: !isFacilitating - }) - + submitting: isModifyingCheckInQuestion, + submitMutation: submitModifyCheckInQuestion, + onError: onModifyCheckInQuestionError, + onCompleted: onModifyCheckInQuestionCompleted + } = useMutationProps() const refresh = () => { UpdateNewCheckInQuestionMutation(atmosphere, { meetingId, checkInQuestion: '' }) + setAiUpdatedIcebreaker('') } + + const updateCheckInQuestionWithGeneratedContent = () => { + submitMutation() + UpdateNewCheckInQuestionMutation(atmosphere, { + meetingId, + checkInQuestion: JSON.stringify( + convertToRaw(ContentState.createFromText(aiUpdatedIcebreaker)) + ) + }) + setAiUpdatedIcebreaker('') + } + + const modify = (modifyType: ModifyType) => { + submitModifyCheckInQuestion() + + const icebreakerToModify = aiUpdatedIcebreaker || checkInQuestion! + ModifyCheckInQuestionMutation( + atmosphere, + { + meetingId, + checkInQuestion: icebreakerToModify, + modifyType + }, + { + onCompleted: (res: ModifyCheckInQuestionMutation$data) => { + onModifyCheckInQuestionCompleted() + const {modifyCheckInQuestion} = res + if (!modifyCheckInQuestion.modifiedCheckInQuestion) { + return + } + + setAiUpdatedIcebreaker(modifyCheckInQuestion.modifiedCheckInQuestion) + }, + onError: onModifyCheckInQuestionError + } + ) + } + + const shouldShowAiIcebreakers = featureFlags?.aiIcebreakers && isFacilitating + return ( - - {/* cannot set min width because iPhone 5 has a width of 320*/} - - {isFacilitating && ( -
- - - - - - - - - - + <> + + {/* cannot set min width because iPhone 5 has a width of 320*/} + + {isFacilitating && ( +
+ + + + + + + + + + +
+ )} + {tooltipPortal(
{tip}
)} +
+ {shouldShowAiIcebreakers && ( +
+
+
+
Modify current icebreaker with AI
+
+
+
As a facilitator, you can spice up the current icebreaker with AI.
+
Others will see the result only if you approve it.
+
+
+ {aiUpdatedIcebreaker &&
{aiUpdatedIcebreaker}
} +
+ + + +
+
+ +
)} - {editIcebreakerTooltipPortal(<>Edit icebreaker)} - {refreshIcebreakerTooltipPortal(<>Refresh icebreaker)} - + ) } diff --git a/packages/client/mutations/ModifyCheckInQuestionMutation.ts b/packages/client/mutations/ModifyCheckInQuestionMutation.ts new file mode 100644 index 00000000000..70f0808f643 --- /dev/null +++ b/packages/client/mutations/ModifyCheckInQuestionMutation.ts @@ -0,0 +1,46 @@ +import graphql from 'babel-plugin-relay/macro' +import {commitMutation} from 'react-relay' +import {StandardMutation} from '../types/relayMutations' +import {ModifyCheckInQuestionMutation as TModifyCheckInQuestionMutation} from '../__generated__/ModifyCheckInQuestionMutation.graphql' + +graphql` + fragment ModifyCheckInQuestionMutation_meeting on ModifyCheckInQuestionSuccess { + modifiedCheckInQuestion + } +` + +const mutation = graphql` + mutation ModifyCheckInQuestionMutation( + $meetingId: ID! + $checkInQuestion: String! + $modifyType: ModifyType! + ) { + modifyCheckInQuestion( + meetingId: $meetingId + checkInQuestion: $checkInQuestion + modifyType: $modifyType + ) { + ... on ErrorPayload { + error { + message + } + } + ...ModifyCheckInQuestionMutation_meeting @relay(mask: false) + } + } +` + +const ModifyCheckInQuestionMutation: StandardMutation = ( + atmosphere, + variables, + {onCompleted, onError} +) => { + return commitMutation(atmosphere, { + mutation, + variables, + onCompleted, + onError + }) +} + +export default ModifyCheckInQuestionMutation diff --git a/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql b/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql index d4c31a4b795..caf4fb8943e 100644 --- a/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql +++ b/packages/server/graphql/private/typeDefs/updateOrgFeatureFlag.graphql @@ -16,6 +16,7 @@ enum OrganizationFeatureFlagsEnum { publicTeams meetingInception kudos + aiIcebreakers } extend type Mutation { diff --git a/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts b/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts new file mode 100644 index 00000000000..68d08947704 --- /dev/null +++ b/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts @@ -0,0 +1,46 @@ +import {getUserId, isTeamMember} from '../../../utils/authorization' + +import {SubscriptionChannel} from 'parabol-client/types/constEnums' +import publish from '../../../utils/publish' + +import {MutationResolvers} from '../resolverTypes' +import getRethink from '../../../database/rethinkDriver' +import standardError from '../../../utils/standardError' +import OpenAIServerManager from '../../../utils/OpenAIServerManager' + +const modifyCheckInQuestion: MutationResolvers['modifyCheckInQuestion'] = async ( + _source, + {meetingId, checkInQuestion, modifyType}, + {authToken, dataLoader, socketId: mutatorId} +) => { + const r = await getRethink() + const operationId = dataLoader.share() + const subOptions = {mutatorId, operationId} + const viewerId = getUserId(authToken) + + // AUTH + const meeting = await r.table('NewMeeting').get(meetingId).run() + if (!meeting) return standardError(new Error('Meeting not found'), {userId: viewerId}) + const {endedAt, teamId} = meeting + if (!isTeamMember(authToken, teamId)) { + return standardError(new Error('Team not found'), {userId: viewerId}) + } + + if (meeting.facilitatorUserId !== viewerId) { + return standardError(new Error('Nice try!'), {userId: viewerId}) + } + + if (endedAt) { + return standardError(new Error('Meeting has already ended'), {userId: viewerId}) + } + + const openai = new OpenAIServerManager() + const modifiedCheckInQuestion = await openai.modifyCheckInQuestion(checkInQuestion, modifyType) + + // RESOLUTION + const data = {modifiedCheckInQuestion} + publish(SubscriptionChannel.MEETING, meetingId, 'ModifyCheckInQuestionSuccess', data, subOptions) + return data +} + +export default modifyCheckInQuestion diff --git a/packages/server/graphql/public/typeDefs/Organization.graphql b/packages/server/graphql/public/typeDefs/Organization.graphql index e34bd856fc1..2601a56a198 100644 --- a/packages/server/graphql/public/typeDefs/Organization.graphql +++ b/packages/server/graphql/public/typeDefs/Organization.graphql @@ -196,4 +196,5 @@ type OrganizationFeatureFlags { publicTeams: Boolean! meetingInception: Boolean! kudos: Boolean! + aiIcebreakers: Boolean! } diff --git a/packages/server/graphql/public/typeDefs/modifyCheckInQuestion.graphql b/packages/server/graphql/public/typeDefs/modifyCheckInQuestion.graphql new file mode 100644 index 00000000000..44d4ba066b8 --- /dev/null +++ b/packages/server/graphql/public/typeDefs/modifyCheckInQuestion.graphql @@ -0,0 +1,25 @@ +enum ModifyType { + SERIOUS + FUNNY + EXCITING +} + +extend type Mutation { + """ + Describe the mutation here + """ + modifyCheckInQuestion( + meetingId: ID! + checkInQuestion: String! + modifyType: ModifyType! + ): ModifyCheckInQuestionPayload! +} + +""" +Return value for modifyCheckInQuestion, which could be an error +""" +union ModifyCheckInQuestionPayload = ErrorPayload | ModifyCheckInQuestionSuccess + +type ModifyCheckInQuestionSuccess { + modifiedCheckInQuestion: String +} diff --git a/packages/server/graphql/public/types/OrganizationFeatureFlags.ts b/packages/server/graphql/public/types/OrganizationFeatureFlags.ts index ef337347fe8..1e4892a4fef 100644 --- a/packages/server/graphql/public/types/OrganizationFeatureFlags.ts +++ b/packages/server/graphql/public/types/OrganizationFeatureFlags.ts @@ -14,7 +14,8 @@ const OrganizationFeatureFlags: OrganizationFeatureFlagsResolvers = { publicTeams: ({publicTeams}) => !!publicTeams, singleColumnStandups: ({singleColumnStandups}) => !!singleColumnStandups, meetingInception: ({meetingInception}) => !!meetingInception, - kudos: ({kudos}) => !!kudos + kudos: ({kudos}) => !!kudos, + aiIcebreakers: ({aiIcebreakers}) => !!aiIcebreakers } export default OrganizationFeatureFlags diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index b2f2542e334..fefcc311a4a 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -1,6 +1,7 @@ import OpenAI from 'openai' import sendToSentry from './sendToSentry' import Reflection from '../database/types/Reflection' +import {ModifyType} from '../graphql/public/resolverTypes' class OpenAIServerManager { private openAIApi @@ -243,6 +244,44 @@ class OpenAIServerManager { return null } } + + async modifyCheckInQuestion(question: string, modifyType: ModifyType) { + if (!this.openAIApi) return null + + const prompt: Record = { + EXCITING: `Transform the following team retrospective ice breaker question into something imaginative and unexpected, using simple and clear language suitable for an international audience. Keep it engaging and thrilling, while ensuring it's easy to understand. + Original question: "${question}"`, + + FUNNY: `Rewrite the following team retrospective ice breaker question to add humor, using straightforward and easy-to-understand language. Aim for a light-hearted, amusing twist that is accessible to an international audience. + Original question: "${question}"`, + + SERIOUS: `Modify the following team retrospective ice breaker question to make it more thought-provoking, using clear and simple language. Make it profound to stimulate insightful discussions, while ensuring it remains comprehensible to a diverse international audience. + Original question: "${question}"` + } + + try { + const response = await this.openAIApi.chat.completions.create({ + model: 'gpt-4', + messages: [ + { + role: 'user', + content: prompt[modifyType] + } + ], + temperature: 0.8, + max_tokens: 256, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0 + }) + + return (response.choices[0]?.message?.content?.trim() as string).replaceAll(`"`, '') ?? null + } catch (e) { + const error = e instanceof Error ? e : new Error('OpenAI failed to modifyCheckInQuestion') + sendToSentry(error) + return null + } + } } export default OpenAIServerManager From 60336c4c01a2e9ddbe6f4275ff67e2e697f3dce2 Mon Sep 17 00:00:00 2001 From: Bartosz Jarocki Date: Wed, 6 Dec 2023 15:43:15 +0100 Subject: [PATCH 2/6] code cleanup, analytics --- .../NewCheckInQuestion.tsx | 158 +++++++++++------- .../UpdateNewCheckInQuestionMutation.ts | 11 +- packages/client/package.json | 1 + packages/client/ui/Button/Button.tsx | 63 +++++++ .../public/mutations/modifyCheckInQuestion.ts | 3 + packages/server/utils/analytics/analytics.ts | 10 ++ yarn.lock | 2 +- 7 files changed, 181 insertions(+), 67 deletions(-) create mode 100644 packages/client/ui/Button/Button.tsx diff --git a/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx b/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx index 4f467dfcd2f..6e49c2819b6 100644 --- a/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx +++ b/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx @@ -17,11 +17,11 @@ import ModifyCheckInQuestionMutation from '../../../../mutations/ModifyCheckInQu import {PALETTE} from '../../../../styles/paletteV3' import convertToTaskContent from '../../../../utils/draftjs/convertToTaskContent' import useMutationProps from '../../../../hooks/useMutationProps' -import clsx from 'clsx' import { ModifyCheckInQuestionMutation$data, ModifyType } from '../../../../__generated__/ModifyCheckInQuestionMutation.graphql' +import {Button} from '../../../../ui/Button/Button' const CogIcon = styled('div')({ color: PALETTE.SLATE_700, @@ -106,7 +106,12 @@ const NewCheckInQuestion = (props: Props) => { } } = meeting const {checkInQuestion} = localPhase + const {viewerId} = atmosphere + const isFacilitating = facilitatorUserId === viewerId + const [editorState, setEditorState] = useEditorState(checkInQuestion) + const {submitting, submitMutation, onCompleted, onError} = useMutationProps() + const updateQuestion = (nextEditorState: EditorState) => { const wasFocused = editorState.getSelection().getHasFocus() const isFocused = nextEditorState.getSelection().getHasFocus() @@ -118,10 +123,14 @@ const NewCheckInQuestion = (props: Props) => { : '' if (nextCheckInQuestion === checkInQuestion) return - UpdateNewCheckInQuestionMutation(atmosphere, { - meetingId, - checkInQuestion: nextCheckInQuestion - }) + UpdateNewCheckInQuestionMutation( + atmosphere, + { + meetingId, + checkInQuestion: nextCheckInQuestion + }, + {onCompleted, onError} + ) } setEditorState(nextEditorState) } @@ -131,14 +140,35 @@ const NewCheckInQuestion = (props: Props) => { const currentText = editorRef.current?.value const nextCheckInQuestion = convertToTaskContent(currentText || '') if (nextCheckInQuestion === checkInQuestion) return - UpdateNewCheckInQuestionMutation(atmosphere, { - meetingId, - checkInQuestion: nextCheckInQuestion - }) + UpdateNewCheckInQuestionMutation( + atmosphere, + { + meetingId, + checkInQuestion: nextCheckInQuestion + }, + {onCompleted, onError} + ) } + const { + tooltipPortal: editIcebreakerTooltipPortal, + openTooltip: openEditIcebreakerTooltip, + closeTooltip: closeEditIcebreakerTooltip, + originRef: editIcebreakerOriginRef + } = useTooltip(MenuPosition.UPPER_CENTER, { + disabled: isEditing || !isFacilitating + }) + const { + tooltipPortal: refreshIcebreakerTooltipPortal, + openTooltip: openRefreshIcebreakerTooltip, + closeTooltip: closeRefreshIcebreakerTooltip, + originRef: refreshIcebreakerOriginRef + } = useTooltip(MenuPosition.UPPER_CENTER, { + disabled: !isFacilitating + }) + const focusQuestion = () => { - closeTooltip() + closeEditIcebreakerTooltip() editorRef.current && editorRef.current.focus() const selection = editorState.getSelection() const contentState = editorState.getCurrentContent() @@ -149,18 +179,7 @@ const NewCheckInQuestion = (props: Props) => { const nextEditorState = EditorState.forceSelection(editorState, jumpToEnd) setEditorState(nextEditorState) } - const {viewerId} = atmosphere - const isFacilitating = facilitatorUserId === viewerId - const tip = 'Tap to customize the Icebreaker' - // eslint-disable-next-line react-hooks/rules-of-hooks - const {tooltipPortal, openTooltip, closeTooltip, originRef} = useTooltip( - MenuPosition.UPPER_CENTER, - { - delay: 300, - disabled: isEditing || !isFacilitating - } - ) - const {submitting, submitMutation} = useMutationProps() + const { submitting: isModifyingCheckInQuestion, submitMutation: submitModifyCheckInQuestion, @@ -168,21 +187,29 @@ const NewCheckInQuestion = (props: Props) => { onCompleted: onModifyCheckInQuestionCompleted } = useMutationProps() const refresh = () => { - UpdateNewCheckInQuestionMutation(atmosphere, { - meetingId, - checkInQuestion: '' - }) + UpdateNewCheckInQuestionMutation( + atmosphere, + { + meetingId, + checkInQuestion: '' + }, + {onCompleted, onError} + ) setAiUpdatedIcebreaker('') } const updateCheckInQuestionWithGeneratedContent = () => { submitMutation() - UpdateNewCheckInQuestionMutation(atmosphere, { - meetingId, - checkInQuestion: JSON.stringify( - convertToRaw(ContentState.createFromText(aiUpdatedIcebreaker)) - ) - }) + UpdateNewCheckInQuestionMutation( + atmosphere, + { + meetingId, + checkInQuestion: JSON.stringify( + convertToRaw(ContentState.createFromText(aiUpdatedIcebreaker)) + ) + }, + {onCompleted, onError} + ) setAiUpdatedIcebreaker('') } @@ -230,24 +257,29 @@ const NewCheckInQuestion = (props: Props) => { {isFacilitating && (
- +
)} - {tooltipPortal(
{tip}
)} {shouldShowAiIcebreakers && (
@@ -262,47 +294,49 @@ const NewCheckInQuestion = (props: Props) => {
{aiUpdatedIcebreaker &&
{aiUpdatedIcebreaker}
}
- - - +
- +
)} + {editIcebreakerTooltipPortal(<>Edit icebreaker)} + {refreshIcebreakerTooltipPortal(<>Refresh icebreaker)} ) } diff --git a/packages/client/mutations/UpdateNewCheckInQuestionMutation.ts b/packages/client/mutations/UpdateNewCheckInQuestionMutation.ts index d2595db270f..c7dc80a21e9 100644 --- a/packages/client/mutations/UpdateNewCheckInQuestionMutation.ts +++ b/packages/client/mutations/UpdateNewCheckInQuestionMutation.ts @@ -1,7 +1,7 @@ import graphql from 'babel-plugin-relay/macro' import {commitMutation} from 'react-relay' import {RecordProxy} from 'relay-runtime' -import {SimpleMutation} from '../types/relayMutations' +import {StandardMutation} from '../types/relayMutations' import {UpdateNewCheckInQuestionMutation as TUpdateNewCheckInQuestionMutation} from '../__generated__/UpdateNewCheckInQuestionMutation.graphql' graphql` fragment UpdateNewCheckInQuestionMutation_meeting on UpdateNewCheckInQuestionPayload { @@ -30,9 +30,10 @@ type CheckInPhase = NonNullable< NonNullable['meeting'] >['phases'][0] -const UpdateNewCheckInQuestionMutation: SimpleMutation = ( +const UpdateNewCheckInQuestionMutation: StandardMutation = ( atmosphere, - variables + variables, + {onCompleted, onError} ) => { return commitMutation(atmosphere, { mutation, @@ -48,7 +49,9 @@ const UpdateNewCheckInQuestionMutation: SimpleMutation phase.getValue('__typename') === 'CheckInPhase' ) as RecordProxy checkInPhase.setValue(checkInQuestion, 'checkInQuestion') - } + }, + onCompleted, + onError }) } diff --git a/packages/client/package.json b/packages/client/package.json index 7262c0589ae..b48b666ce29 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -81,6 +81,7 @@ "@radix-ui/react-scroll-area": "^1.0.3", "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-tooltip": "^1.0.7", + "@radix-ui/react-slot": "^1.0.2", "@sentry/browser": "^5.8.0", "@stripe/react-stripe-js": "^1.16.5", "@stripe/stripe-js": "^1.47.0", diff --git a/packages/client/ui/Button/Button.tsx b/packages/client/ui/Button/Button.tsx new file mode 100644 index 00000000000..a60c67700d7 --- /dev/null +++ b/packages/client/ui/Button/Button.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import {Slot} from '@radix-ui/react-slot' +import clsx from 'clsx' + +type Variant = 'primary' | 'secondary' | 'destructive' | 'ghost' | 'link' | 'outline' +type Size = 'sm' | 'md' | 'lg' | 'default' +type Shape = 'pill' | 'circle' | 'default' + +const BASE_STYLES = + 'cursor-pointer inline-flex items-center justify-center whitespace-nowrap font-semibold transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50' + +// TODO: make sure the styles match the designs +const VARIANT_STYLES: Record = { + primary: 'bg-primary text-white hover:bg-primary/90', + destructive: 'bg-tomato-500 text-white hover:bg-tomato-500/90', + outline: 'text-slate-900 border border-slate-400 hover:bg-slate-200 px-2.5 py-1 bg-transparent', + secondary: 'bg-sky-500 text-white hover:bg-sky-500/80', + ghost: 'hover:bg-accent', + link: 'text-primary underline-offset-4 hover:underline' +} + +const SIZE_STYLES: Record = { + default: 'px-4 py-2 text-xs', + sm: 'h-7 px-3 text-xs', + md: 'h-9 px-4 text-sm', + lg: 'h-11 px-8 text-base' +} + +const SHAPE_STYLES: Record = { + pill: 'rounded-full', + circle: 'rounded-full aspect-square', + default: 'rounded-md' +} + +export interface ButtonProps extends React.ButtonHTMLAttributes { + asChild?: boolean + variant: Variant + size?: Size + shape: Shape +} + +const Button = React.forwardRef( + ({className, variant, size, shape, asChild = false, ...props}, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) + +Button.displayName = 'Button' + +export {Button} diff --git a/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts b/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts index 68d08947704..581e197d550 100644 --- a/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts +++ b/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts @@ -7,6 +7,7 @@ import {MutationResolvers} from '../resolverTypes' import getRethink from '../../../database/rethinkDriver' import standardError from '../../../utils/standardError' import OpenAIServerManager from '../../../utils/OpenAIServerManager' +import {analytics} from '../../../utils/analytics/analytics' const modifyCheckInQuestion: MutationResolvers['modifyCheckInQuestion'] = async ( _source, @@ -37,6 +38,8 @@ const modifyCheckInQuestion: MutationResolvers['modifyCheckInQuestion'] = async const openai = new OpenAIServerManager() const modifiedCheckInQuestion = await openai.modifyCheckInQuestion(checkInQuestion, modifyType) + analytics.icebreakerModified(viewerId, meetingId, modifyType) + // RESOLUTION const data = {modifiedCheckInQuestion} publish(SubscriptionChannel.MEETING, meetingId, 'ModifyCheckInQuestionSuccess', data, subOptions) diff --git a/packages/server/utils/analytics/analytics.ts b/packages/server/utils/analytics/analytics.ts index 60c480bbe29..43b94aa46b3 100644 --- a/packages/server/utils/analytics/analytics.ts +++ b/packages/server/utils/analytics/analytics.ts @@ -19,6 +19,7 @@ import {AmplitudeAnalytics} from './amplitude/AmplitudeAnalytics' import {createMeetingProperties} from './helpers' import {SlackNotificationEventEnum} from '../../database/types/SlackNotification' import TemplateScale from '../../database/types/TemplateScale' +import {ModifyType} from '../../graphql/public/resolverTypes' export type MeetingSeriesAnalyticsProperties = Pick< MeetingSeries, @@ -163,6 +164,7 @@ export type AnalyticsEvent = | 'Conversion Modal Pay Later Clicked' // kudos | 'Kudos Sent' + | 'Icebreaker modified' // Deprecated Events // These will be replaced with tracking plan compliant versions by the data team // Lowercase words are for backwards compatibility @@ -677,6 +679,14 @@ class Analytics { }) } + icebreakerModified = (userId: string, meetingId: string, modifyType: ModifyType) => { + this.track(userId, 'Icebreaker modified', { + userId, + meetingId, + modifyType + }) + } + identify = (options: IdentifyOptions) => { this.amplitudeAnalytics.identify(options) } diff --git a/yarn.lock b/yarn.lock index f14a5aacd2a..46e3970e270 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7261,7 +7261,7 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.0" -"@radix-ui/react-slot@1.0.2": +"@radix-ui/react-slot@1.0.2", "@radix-ui/react-slot@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg== From 7cb71dd6cfe8312da95a97ae1c8450fef9508bc0 Mon Sep 17 00:00:00 2001 From: Bartosz Jarocki Date: Wed, 6 Dec 2023 21:18:54 +0100 Subject: [PATCH 3/6] code cleanup --- packages/server/utils/analytics/analytics.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/utils/analytics/analytics.ts b/packages/server/utils/analytics/analytics.ts index 43b94aa46b3..59b1f5a03dd 100644 --- a/packages/server/utils/analytics/analytics.ts +++ b/packages/server/utils/analytics/analytics.ts @@ -164,7 +164,7 @@ export type AnalyticsEvent = | 'Conversion Modal Pay Later Clicked' // kudos | 'Kudos Sent' - | 'Icebreaker modified' + | 'Icebreaker Modified' // Deprecated Events // These will be replaced with tracking plan compliant versions by the data team // Lowercase words are for backwards compatibility @@ -680,7 +680,7 @@ class Analytics { } icebreakerModified = (userId: string, meetingId: string, modifyType: ModifyType) => { - this.track(userId, 'Icebreaker modified', { + this.track(userId, 'Icebreaker Modified', { userId, meetingId, modifyType From 8ceee950bc8520b2532d74d90a82345a1e20c1dc Mon Sep 17 00:00:00 2001 From: Bartosz Jarocki Date: Wed, 6 Dec 2023 22:02:07 +0100 Subject: [PATCH 4/6] fix: added success property to icebreaker modified analytics event --- .../graphql/public/mutations/modifyCheckInQuestion.ts | 2 +- packages/server/utils/analytics/analytics.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts b/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts index 581e197d550..8c714e6ff78 100644 --- a/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts +++ b/packages/server/graphql/public/mutations/modifyCheckInQuestion.ts @@ -38,7 +38,7 @@ const modifyCheckInQuestion: MutationResolvers['modifyCheckInQuestion'] = async const openai = new OpenAIServerManager() const modifiedCheckInQuestion = await openai.modifyCheckInQuestion(checkInQuestion, modifyType) - analytics.icebreakerModified(viewerId, meetingId, modifyType) + analytics.icebreakerModified(viewerId, meetingId, modifyType, modifiedCheckInQuestion !== null) // RESOLUTION const data = {modifiedCheckInQuestion} diff --git a/packages/server/utils/analytics/analytics.ts b/packages/server/utils/analytics/analytics.ts index 59b1f5a03dd..52ff505604f 100644 --- a/packages/server/utils/analytics/analytics.ts +++ b/packages/server/utils/analytics/analytics.ts @@ -679,11 +679,17 @@ class Analytics { }) } - icebreakerModified = (userId: string, meetingId: string, modifyType: ModifyType) => { + icebreakerModified = ( + userId: string, + meetingId: string, + modifyType: ModifyType, + success: boolean + ) => { this.track(userId, 'Icebreaker Modified', { userId, meetingId, - modifyType + modifyType, + success }) } From e1adbce659d03257fac8d22b5084488795f62855 Mon Sep 17 00:00:00 2001 From: Bartosz Jarocki Date: Mon, 11 Dec 2023 10:44:53 +0100 Subject: [PATCH 5/6] refactor: use relay mutation helper for modify check in question mutation --- .../NewCheckInQuestion.tsx | 50 +++++++------------ .../ModifyCheckInQuestionMutation.ts | 46 ----------------- .../useModifyCheckInQuestionMutation.ts | 38 ++++++++++++++ 3 files changed, 57 insertions(+), 77 deletions(-) delete mode 100644 packages/client/mutations/ModifyCheckInQuestionMutation.ts create mode 100644 packages/client/mutations/useModifyCheckInQuestionMutation.ts diff --git a/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx b/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx index 6e49c2819b6..dac4215e2e2 100644 --- a/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx +++ b/packages/client/modules/meeting/components/MeetingCheckInPrompt/NewCheckInQuestion.tsx @@ -13,15 +13,15 @@ import {MenuPosition} from '../../../../hooks/useCoords' import useEditorState from '../../../../hooks/useEditorState' import useTooltip from '../../../../hooks/useTooltip' import UpdateNewCheckInQuestionMutation from '../../../../mutations/UpdateNewCheckInQuestionMutation' -import ModifyCheckInQuestionMutation from '../../../../mutations/ModifyCheckInQuestionMutation' import {PALETTE} from '../../../../styles/paletteV3' import convertToTaskContent from '../../../../utils/draftjs/convertToTaskContent' import useMutationProps from '../../../../hooks/useMutationProps' import { - ModifyCheckInQuestionMutation$data, + useModifyCheckInQuestionMutation$data as TModifyCheckInQuestion$data, ModifyType -} from '../../../../__generated__/ModifyCheckInQuestionMutation.graphql' +} from '../../../../__generated__/useModifyCheckInQuestionMutation.graphql' import {Button} from '../../../../ui/Button/Button' +import {useModifyCheckInQuestionMutation} from '../../../../mutations/useModifyCheckInQuestionMutation' const CogIcon = styled('div')({ color: PALETTE.SLATE_700, @@ -180,12 +180,6 @@ const NewCheckInQuestion = (props: Props) => { setEditorState(nextEditorState) } - const { - submitting: isModifyingCheckInQuestion, - submitMutation: submitModifyCheckInQuestion, - onError: onModifyCheckInQuestionError, - onCompleted: onModifyCheckInQuestionCompleted - } = useMutationProps() const refresh = () => { UpdateNewCheckInQuestionMutation( atmosphere, @@ -213,32 +207,26 @@ const NewCheckInQuestion = (props: Props) => { setAiUpdatedIcebreaker('') } - const modify = (modifyType: ModifyType) => { - submitModifyCheckInQuestion() - + const [executeModifyCheckInQuestionMutation, isModifyingCheckInQuestion] = + useModifyCheckInQuestionMutation() + const modifyCheckInQuestion = (modifyType: ModifyType) => { const icebreakerToModify = aiUpdatedIcebreaker || checkInQuestion! - ModifyCheckInQuestionMutation( - atmosphere, - { + executeModifyCheckInQuestionMutation({ + variables: { meetingId, checkInQuestion: icebreakerToModify, modifyType }, - { - onCompleted: (res: ModifyCheckInQuestionMutation$data) => { - onModifyCheckInQuestionCompleted() - const {modifyCheckInQuestion} = res - if (!modifyCheckInQuestion.modifiedCheckInQuestion) { - return - } + onCompleted: (res: TModifyCheckInQuestion$data) => { + const {modifyCheckInQuestion} = res + if (!modifyCheckInQuestion.modifiedCheckInQuestion) { + return + } - setAiUpdatedIcebreaker(modifyCheckInQuestion.modifiedCheckInQuestion) - }, - onError: onModifyCheckInQuestionError + setAiUpdatedIcebreaker(modifyCheckInQuestion.modifiedCheckInQuestion) } - ) + }) } - const shouldShowAiIcebreakers = featureFlags?.aiIcebreakers && isFacilitating return ( @@ -282,7 +270,7 @@ const NewCheckInQuestion = (props: Props) => { )}
{shouldShowAiIcebreakers && ( -
+
Modify current icebreaker with AI
@@ -299,7 +287,7 @@ const NewCheckInQuestion = (props: Props) => { shape='pill' size='sm' disabled={isModifyingCheckInQuestion} - onClick={() => modify('SERIOUS')} + onClick={() => modifyCheckInQuestion('SERIOUS')} > More serious @@ -308,7 +296,7 @@ const NewCheckInQuestion = (props: Props) => { shape='pill' size='sm' disabled={isModifyingCheckInQuestion} - onClick={() => modify('FUNNY')} + onClick={() => modifyCheckInQuestion('FUNNY')} > More funny @@ -317,7 +305,7 @@ const NewCheckInQuestion = (props: Props) => { shape='pill' size='sm' disabled={isModifyingCheckInQuestion} - onClick={() => modify('EXCITING')} + onClick={() => modifyCheckInQuestion('EXCITING')} > More exciting diff --git a/packages/client/mutations/ModifyCheckInQuestionMutation.ts b/packages/client/mutations/ModifyCheckInQuestionMutation.ts deleted file mode 100644 index 70f0808f643..00000000000 --- a/packages/client/mutations/ModifyCheckInQuestionMutation.ts +++ /dev/null @@ -1,46 +0,0 @@ -import graphql from 'babel-plugin-relay/macro' -import {commitMutation} from 'react-relay' -import {StandardMutation} from '../types/relayMutations' -import {ModifyCheckInQuestionMutation as TModifyCheckInQuestionMutation} from '../__generated__/ModifyCheckInQuestionMutation.graphql' - -graphql` - fragment ModifyCheckInQuestionMutation_meeting on ModifyCheckInQuestionSuccess { - modifiedCheckInQuestion - } -` - -const mutation = graphql` - mutation ModifyCheckInQuestionMutation( - $meetingId: ID! - $checkInQuestion: String! - $modifyType: ModifyType! - ) { - modifyCheckInQuestion( - meetingId: $meetingId - checkInQuestion: $checkInQuestion - modifyType: $modifyType - ) { - ... on ErrorPayload { - error { - message - } - } - ...ModifyCheckInQuestionMutation_meeting @relay(mask: false) - } - } -` - -const ModifyCheckInQuestionMutation: StandardMutation = ( - atmosphere, - variables, - {onCompleted, onError} -) => { - return commitMutation(atmosphere, { - mutation, - variables, - onCompleted, - onError - }) -} - -export default ModifyCheckInQuestionMutation diff --git a/packages/client/mutations/useModifyCheckInQuestionMutation.ts b/packages/client/mutations/useModifyCheckInQuestionMutation.ts new file mode 100644 index 00000000000..ce2ee15c5b0 --- /dev/null +++ b/packages/client/mutations/useModifyCheckInQuestionMutation.ts @@ -0,0 +1,38 @@ +import graphql from 'babel-plugin-relay/macro' +import {useMutation, UseMutationConfig} from 'react-relay' +import {useModifyCheckInQuestionMutation as TModifyCheckInQuestionMutation} from '../__generated__/useModifyCheckInQuestionMutation.graphql' + +graphql` + fragment useModifyCheckInQuestionMutation_success on ModifyCheckInQuestionSuccess { + modifiedCheckInQuestion + } +` + +const mutation = graphql` + mutation useModifyCheckInQuestionMutation( + $meetingId: ID! + $checkInQuestion: String! + $modifyType: ModifyType! + ) { + modifyCheckInQuestion( + meetingId: $meetingId + checkInQuestion: $checkInQuestion + modifyType: $modifyType + ) { + ... on ErrorPayload { + error { + message + } + } + ...useModifyCheckInQuestionMutation_success @relay(mask: false) + } + } +` + +export const useModifyCheckInQuestionMutation = () => { + const [commit, submitting] = useMutation(mutation) + const execute = (config: UseMutationConfig) => { + return commit(config) + } + return [execute, submitting] as const +} From cc9e17e5704d9ab275438b79fda195f85ca30a94 Mon Sep 17 00:00:00 2001 From: Bartosz Jarocki Date: Mon, 11 Dec 2023 10:45:24 +0100 Subject: [PATCH 6/6] feat: add modified question char limit --- packages/server/utils/OpenAIServerManager.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index fefcc311a4a..73721dcfa2d 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -248,14 +248,15 @@ class OpenAIServerManager { async modifyCheckInQuestion(question: string, modifyType: ModifyType) { if (!this.openAIApi) return null + const maxQuestionLength = 160 const prompt: Record = { - EXCITING: `Transform the following team retrospective ice breaker question into something imaginative and unexpected, using simple and clear language suitable for an international audience. Keep it engaging and thrilling, while ensuring it's easy to understand. + EXCITING: `Transform the following team retrospective ice breaker question into something imaginative and unexpected, using simple and clear language suitable for an international audience. Keep it engaging and thrilling, while ensuring it's easy to understand. Ensure the modified question does not exceed ${maxQuestionLength} characters. Original question: "${question}"`, - FUNNY: `Rewrite the following team retrospective ice breaker question to add humor, using straightforward and easy-to-understand language. Aim for a light-hearted, amusing twist that is accessible to an international audience. + FUNNY: `Rewrite the following team retrospective ice breaker question to add humor, using straightforward and easy-to-understand language. Aim for a light-hearted, amusing twist that is accessible to an international audience. Ensure the modified question does not exceed ${maxQuestionLength} characters. Original question: "${question}"`, - SERIOUS: `Modify the following team retrospective ice breaker question to make it more thought-provoking, using clear and simple language. Make it profound to stimulate insightful discussions, while ensuring it remains comprehensible to a diverse international audience. + SERIOUS: `Modify the following team retrospective ice breaker question to make it more thought-provoking, using clear and simple language. Make it profound to stimulate insightful discussions, while ensuring it remains comprehensible to a diverse international audience. Ensure the modified question does not exceed ${maxQuestionLength} characters. Original question: "${question}"` }