Skip to content

Commit

Permalink
feat: added option to modify icebreakers with ai (#9268)
Browse files Browse the repository at this point in the history
* feat: added option to modify icebreakers with ai

* code cleanup, analytics

* code cleanup

* fix: added success property to icebreaker modified analytics event

* refactor: use relay mutation helper for modify check in question mutation

* feat: add modified question char limit
  • Loading branch information
BartoszJarocki authored Dec 11, 2023
1 parent 8cbc121 commit 70db85f
Show file tree
Hide file tree
Showing 13 changed files with 444 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -15,6 +15,13 @@ import useTooltip from '../../../../hooks/useTooltip'
import UpdateNewCheckInQuestionMutation from '../../../../mutations/UpdateNewCheckInQuestionMutation'
import {PALETTE} from '../../../../styles/paletteV3'
import convertToTaskContent from '../../../../utils/draftjs/convertToTaskContent'
import useMutationProps from '../../../../hooks/useMutationProps'
import {
useModifyCheckInQuestionMutation$data as TModifyCheckInQuestion$data,
ModifyType
} from '../../../../__generated__/useModifyCheckInQuestionMutation.graphql'
import {Button} from '../../../../ui/Button/Button'
import {useModifyCheckInQuestionMutation} from '../../../../mutations/useModifyCheckInQuestionMutation'

const CogIcon = styled('div')({
color: PALETTE.SLATE_700,
Expand Down Expand Up @@ -77,14 +84,34 @@ 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 {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()
Expand All @@ -94,11 +121,16 @@ const NewCheckInQuestion = (props: Props) => {
const nextCheckInQuestion = nextContent.hasText()
? JSON.stringify(convertToRaw(nextContent))
: ''

if (nextCheckInQuestion === checkInQuestion) return
UpdateNewCheckInQuestionMutation(atmosphere, {
meetingId,
checkInQuestion: nextCheckInQuestion
})
UpdateNewCheckInQuestionMutation(
atmosphere,
{
meetingId,
checkInQuestion: nextCheckInQuestion
},
{onCompleted, onError}
)
}
setEditorState(nextEditorState)
}
Expand All @@ -108,27 +140,16 @@ 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 focusQuestion = () => {
closeEditIcebreakerTooltip()
editorRef.current && editorRef.current.focus()
const selection = editorState.getSelection()
const contentState = editorState.getCurrentContent()
const jumpToEnd = (selection as any).merge({
anchorOffset: contentState.getLastBlock().getLength(),
focusOffset: contentState.getLastBlock().getLength()
}) as SelectionState
const nextEditorState = EditorState.forceSelection(editorState, jumpToEnd)
setEditorState(nextEditorState)
}
const {viewerId} = atmosphere
const isFacilitating = facilitatorUserId === viewerId
// eslint-disable-next-line react-hooks/rules-of-hooks
const {
tooltipPortal: editIcebreakerTooltipPortal,
openTooltip: openEditIcebreakerTooltip,
Expand All @@ -146,53 +167,165 @@ const NewCheckInQuestion = (props: Props) => {
disabled: !isFacilitating
})

const focusQuestion = () => {
closeEditIcebreakerTooltip()
editorRef.current && editorRef.current.focus()
const selection = editorState.getSelection()
const contentState = editorState.getCurrentContent()
const jumpToEnd = (selection as any).merge({
anchorOffset: contentState.getLastBlock().getLength(),
focusOffset: contentState.getLastBlock().getLength()
}) as SelectionState
const nextEditorState = EditorState.forceSelection(editorState, jumpToEnd)
setEditorState(nextEditorState)
}

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))
)
},
{onCompleted, onError}
)
setAiUpdatedIcebreaker('')
}

const [executeModifyCheckInQuestionMutation, isModifyingCheckInQuestion] =
useModifyCheckInQuestionMutation()
const modifyCheckInQuestion = (modifyType: ModifyType) => {
const icebreakerToModify = aiUpdatedIcebreaker || checkInQuestion!
executeModifyCheckInQuestionMutation({
variables: {
meetingId,
checkInQuestion: icebreakerToModify,
modifyType
},
onCompleted: (res: TModifyCheckInQuestion$data) => {
const {modifyCheckInQuestion} = res
if (!modifyCheckInQuestion.modifiedCheckInQuestion) {
return
}

setAiUpdatedIcebreaker(modifyCheckInQuestion.modifiedCheckInQuestion)
}
})
}
const shouldShowAiIcebreakers = featureFlags?.aiIcebreakers && isFacilitating

return (
<QuestionBlock>
{/* cannot set min width because iPhone 5 has a width of 320*/}
<EditorInputWrapper
ariaLabel={'Edit the icebreaker'}
editorState={editorState}
setEditorState={updateQuestion}
readOnly={!isFacilitating}
placeholder='e.g. How are you?'
editorRef={editorRef}
setEditorStateFallback={updateQuestionAndroidFallback}
/>
{isFacilitating && (
<div className='flex gap-x-2'>
<PlainButton
aria-label={'Edit icebreaker'}
onClick={focusQuestion}
onMouseEnter={openEditIcebreakerTooltip}
onMouseLeave={closeEditIcebreakerTooltip}
ref={editIcebreakerOriginRef}
>
<CogIcon>
<CreateIcon />
</CogIcon>
</PlainButton>
<PlainButton
aria-label={'Refresh icebreaker'}
onClick={refresh}
onMouseEnter={openRefreshIcebreakerTooltip}
onMouseLeave={closeRefreshIcebreakerTooltip}
ref={refreshIcebreakerOriginRef}
>
<CogIcon>
<RefreshIcon />
</CogIcon>
</PlainButton>
<>
<QuestionBlock id='test'>
{/* cannot set min width because iPhone 5 has a width of 320*/}
<EditorInputWrapper
ariaLabel={'Edit the icebreaker'}
editorState={editorState}
setEditorState={updateQuestion}
readOnly={!isFacilitating}
placeholder='e.g. How are you?'
editorRef={editorRef}
setEditorStateFallback={updateQuestionAndroidFallback}
/>
{isFacilitating && (
<div className='flex gap-x-2'>
<PlainButton
aria-label={'Edit icebreaker'}
onClick={focusQuestion}
onMouseEnter={openEditIcebreakerTooltip}
onMouseLeave={closeEditIcebreakerTooltip}
ref={editIcebreakerOriginRef}
>
<CogIcon>
<CreateIcon />
</CogIcon>
</PlainButton>
<PlainButton
aria-label={'Refresh icebreaker'}
onClick={refresh}
onMouseEnter={openRefreshIcebreakerTooltip}
onMouseLeave={closeRefreshIcebreakerTooltip}
ref={refreshIcebreakerOriginRef}
>
<CogIcon>
<RefreshIcon />
</CogIcon>
</PlainButton>
</div>
)}
</QuestionBlock>
{shouldShowAiIcebreakers && (
<div className='flex flex-col gap-4 rounded-lg bg-slate-100 p-3'>
<div className='flex flex-col items-center justify-center gap-2'>
<div className='inline-flex gap-2'>
<div className='font-semibold'>Modify current icebreaker with AI</div>
</div>
<div className='text-center text-sm italic'>
<div>As a facilitator, you can spice up the current icebreaker with AI.</div>
<div>Others will see the result only if you approve it.</div>
</div>
</div>
{aiUpdatedIcebreaker && <div className='p-2 text-center'>{aiUpdatedIcebreaker}</div>}
<div className='flex items-center justify-center gap-x-3'>
<Button
variant='outline'
shape='pill'
size='sm'
disabled={isModifyingCheckInQuestion}
onClick={() => modifyCheckInQuestion('SERIOUS')}
>
More serious
</Button>
<Button
variant='outline'
shape='pill'
size='sm'
disabled={isModifyingCheckInQuestion}
onClick={() => modifyCheckInQuestion('FUNNY')}
>
More funny
</Button>
<Button
variant='outline'
shape='pill'
size='sm'
disabled={isModifyingCheckInQuestion}
onClick={() => modifyCheckInQuestion('EXCITING')}
>
More exciting
</Button>
</div>
<div className='flex items-center justify-center gap-x-3'>
<Button
variant='secondary'
shape='pill'
size='md'
disabled={aiUpdatedIcebreaker === '' || isModifyingCheckInQuestion || submitting}
onClick={updateCheckInQuestionWithGeneratedContent}
>
{isModifyingCheckInQuestion ? 'Modifying...' : 'Approve'}
</Button>
</div>
</div>
)}
{editIcebreakerTooltipPortal(<>Edit icebreaker</>)}
{refreshIcebreakerTooltipPortal(<>Refresh icebreaker</>)}
</QuestionBlock>
</>
)
}

Expand Down
11 changes: 7 additions & 4 deletions packages/client/mutations/UpdateNewCheckInQuestionMutation.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -30,9 +30,10 @@ type CheckInPhase = NonNullable<
NonNullable<TUpdateNewCheckInQuestionMutation['response']['updateNewCheckInQuestion']>['meeting']
>['phases'][0]

const UpdateNewCheckInQuestionMutation: SimpleMutation<TUpdateNewCheckInQuestionMutation> = (
const UpdateNewCheckInQuestionMutation: StandardMutation<TUpdateNewCheckInQuestionMutation> = (
atmosphere,
variables
variables,
{onCompleted, onError}
) => {
return commitMutation(atmosphere, {
mutation,
Expand All @@ -48,7 +49,9 @@ const UpdateNewCheckInQuestionMutation: SimpleMutation<TUpdateNewCheckInQuestion
(phase) => phase.getValue('__typename') === 'CheckInPhase'
) as RecordProxy<CheckInPhase>
checkInPhase.setValue(checkInQuestion, 'checkInQuestion')
}
},
onCompleted,
onError
})
}

Expand Down
38 changes: 38 additions & 0 deletions packages/client/mutations/useModifyCheckInQuestionMutation.ts
Original file line number Diff line number Diff line change
@@ -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<TModifyCheckInQuestionMutation>(mutation)
const execute = (config: UseMutationConfig<TModifyCheckInQuestionMutation>) => {
return commit(config)
}
return [execute, submitting] as const
}
Loading

0 comments on commit 70db85f

Please sign in to comment.