Skip to content

Commit

Permalink
feat: improve reflection group titles (#10546)
Browse files Browse the repository at this point in the history
  • Loading branch information
nickoferrall authored Dec 10, 2024
1 parent 2741491 commit cfde723
Show file tree
Hide file tree
Showing 8 changed files with 185 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {PALETTE} from '../../styles/paletteV3'
import ui from '../../styles/ui'
import {Card} from '../../types/constEnums'
import {RETRO_TOPIC_LABEL} from '../../utils/constants'
import Ellipsis from '../Ellipsis/Ellipsis'
import StyledError from '../StyledError'

interface Props {
Expand Down Expand Up @@ -127,6 +128,7 @@ const ReflectionGroupTitleEditor = (props: Props) => {
const {id: reflectionGroupId, title} = reflectionGroup
const dirtyRef = useRef(false)
const initialTitleRef = useRef(title)
const isLoading = title === ''

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const title = e.target.value
Expand Down Expand Up @@ -178,19 +180,25 @@ const ReflectionGroupTitleEditor = (props: Props) => {
<InputWithIconWrap>
<RootBlock data-cy='group-title-editor'>
<FormBlock onSubmit={onSubmit}>
<NameInput
data-cy='group-title-editor-input'
isExpanded={isExpanded}
onBlur={onSubmit}
onChange={onChange}
onKeyPress={onKeyPress}
placeholder={RETRO_TOPIC_LABEL}
readOnly={readOnly}
ref={titleInputRef}
maxLength={200}
type='text'
value={title || ''}
/>
{isLoading ? (
<span className='inline-block text-left font-semibold'>
<Ellipsis />
</span>
) : (
<NameInput
data-cy='group-title-editor-input'
isExpanded={isExpanded}
onBlur={onSubmit}
onChange={onChange}
onKeyPress={onKeyPress}
placeholder={RETRO_TOPIC_LABEL}
readOnly={readOnly}
ref={titleInputRef}
maxLength={200}
type='text'
value={title || ''}
/>
)}
</FormBlock>
{error && <StyledError>{error.message}</StyledError>}
</RootBlock>
Expand Down
7 changes: 6 additions & 1 deletion packages/client/mutations/EndDraggingReflectionMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ const EndDraggingReflectionMutation: SimpleMutation<TEndDraggingReflectionMutati
const oldReflectionGroupId = reflection.getValue('reflectionGroupId') as string
let reflectionGroupProxy: RecordProxy<{meetingId: string}>
const newReflectionGroupId = clientTempId()

// move a reflection into its own group
if (!reflectionGroupId) {
// create the new group
Expand All @@ -179,7 +180,9 @@ const EndDraggingReflectionMutation: SimpleMutation<TEndDraggingReflectionMutati
meetingId: reflection.getValue('meetingId') as string,
isActive: true,
sortOrder: 0,
updatedAt: nowISO
updatedAt: nowISO,
title: '',
smartTitle: ''
}
reflectionGroupProxy = createProxyRecord(store, 'RetroReflectionGroup', reflectionGroup)
updateProxyRecord(reflection, {sortOrder: 0, reflectionGroupId: newReflectionGroupId})
Expand All @@ -197,6 +200,8 @@ const EndDraggingReflectionMutation: SimpleMutation<TEndDraggingReflectionMutati
reflectionGroupId
})
reflection.setLinkedRecord(reflectionGroupProxy, 'retroReflectionGroup')
reflectionGroupProxy.setValue('', 'title')
reflectionGroupProxy.setValue('', 'smartTitle')
}
moveReflectionLocation(reflection, reflectionGroupProxy, oldReflectionGroupId, store)
}
Expand Down
38 changes: 38 additions & 0 deletions packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {SubscriptionChannel} from 'parabol-client/types/constEnums'
import OpenAIServerManager from '../../../utils/OpenAIServerManager'
import publish from '../../../utils/publish'
import standardError from '../../../utils/standardError'
import {DataLoaderWorker} from '../../graphql'
import updateSmartGroupTitle from './updateReflectionLocation/updateSmartGroupTitle'

interface Reflection {
entities: any[]
plaintextContent: string
}

const generateAIGroupTitle = async (
reflections: Reflection[],
reflectionGroupId: string,
meetingId: string,
dataLoader: DataLoaderWorker
) => {
const manager = new OpenAIServerManager()
const aiTitle = await manager.generateGroupTitle(reflections)
const newTitle = aiTitle ?? reflections[0]?.plaintextContent ?? ''
if (!newTitle) standardError(new Error('Failed to generate AI title'))
await updateSmartGroupTitle(reflectionGroupId, newTitle)
dataLoader.get('retroReflectionGroups').clear(reflectionGroupId)
publish(
SubscriptionChannel.MEETING,
meetingId,
'UpdateReflectionGroupTitlePayload',
{
meetingId,
reflectionGroupId,
title: aiTitle
},
{operationId: dataLoader.share()}
)
}

export default generateAIGroupTitle
42 changes: 42 additions & 0 deletions packages/server/graphql/mutations/helpers/updateGroupTitle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import getGroupSmartTitle from '../../../../client/utils/smartGroup/getGroupSmartTitle'
import getKysely from '../../../postgres/getKysely'
import {DataLoaderWorker} from '../../graphql'
import {RetroReflection} from '../../public/resolverTypes'
import canAccessAI from './canAccessAI'
import generateAIGroupTitle from './generateAIGroupTitle'
import updateSmartGroupTitle from './updateReflectionLocation/updateSmartGroupTitle'

type Input = {
reflections: RetroReflection[]
reflectionGroupId: string
meetingId: string
teamId: string
dataLoader: DataLoaderWorker
}

const updateGroupTitle = async (input: Input) => {
const {reflections, reflectionGroupId, meetingId, teamId, dataLoader} = input
if (reflections.length === 1) {
// For single reflection, use its content as the title
const newTitle = reflections[0].plaintextContent
await updateSmartGroupTitle(reflectionGroupId, newTitle)
return
}
const team = await dataLoader.get('teams').loadNonNull(teamId)
const hasAIAccess = await canAccessAI(team, 'retrospective', dataLoader)
if (!hasAIAccess) {
const smartTitle = getGroupSmartTitle(reflections)
await updateSmartGroupTitle(reflectionGroupId, smartTitle)
} else {
const pg = getKysely()
await pg
.updateTable('RetroReflectionGroup')
.set({title: '', smartTitle: ''})
.where('id', '=', reflectionGroupId)
.execute()
// Generate title and don't await or the reflection will hang when it's dropped
generateAIGroupTitle(reflections, reflectionGroupId, meetingId, dataLoader)
}
}

export default updateGroupTitle
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import dndNoise from 'parabol-client/utils/dndNoise'
import getGroupSmartTitle from 'parabol-client/utils/smartGroup/getGroupSmartTitle'
import getKysely from '../../../../postgres/getKysely'
import updateGroupTitle from '../updateGroupTitle'
import {GQLContext} from './../../../graphql'
import updateSmartGroupTitle from './updateSmartGroupTitle'

const addReflectionToGroup = async (
reflectionId: string,
Expand Down Expand Up @@ -51,30 +50,43 @@ const addReflectionToGroup = async (
reflection.updatedAt = now

if (oldReflectionGroupId !== reflectionGroupId) {
// ths is not just a reorder within the same group
// this is not just a reorder within the same group
const nextReflections = [...reflectionsInNextGroup, reflection]
const oldReflections = await dataLoader
.get('retroReflectionsByGroupId')
.load(oldReflectionGroupId)

const nextTitle = smartTitle ?? getGroupSmartTitle(nextReflections)
const oldGroupHasSingleReflectionCustomTitle =
oldReflectionGroup.title !== oldReflectionGroup.smartTitle && oldReflections.length === 0
const newGroupHasSmartTitle = reflectionGroup.title === reflectionGroup.smartTitle

if (oldGroupHasSingleReflectionCustomTitle && newGroupHasSmartTitle) {
// Edge case of dragging a single card with a custom group name on a group with smart name
await pg
.updateTable('RetroReflectionGroup')
.set({title: oldReflectionGroup.title, smartTitle: nextTitle})
.set({title: oldReflectionGroup.title, smartTitle: smartTitle ?? ''})
.where('id', '=', reflectionGroupId)
.execute()
} else {
await updateSmartGroupTitle(reflectionGroupId, nextTitle)
const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId)
await updateGroupTitle({
reflections: nextReflections,
reflectionGroupId,
meetingId,
teamId: meeting.teamId,
dataLoader
})
}

if (oldReflections.length > 0) {
const oldTitle = getGroupSmartTitle(oldReflections)
await updateSmartGroupTitle(oldReflectionGroupId, oldTitle)
const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId)
await updateGroupTitle({
reflections: oldReflections,
reflectionGroupId: oldReflectionGroupId,
meetingId,
teamId: meeting.teamId,
dataLoader
})
} else {
await pg
.updateTable('RetroReflectionGroup')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import getGroupSmartTitle from 'parabol-client/utils/smartGroup/getGroupSmartTitle'
import dndNoise from '../../../../../client/utils/dndNoise'
import ReflectionGroup from '../../../../database/types/ReflectionGroup'
import getKysely from '../../../../postgres/getKysely'
import {GQLContext} from '../../../graphql'
import updateSmartGroupTitle from './updateSmartGroupTitle'
import updateGroupTitle from '../updateGroupTitle'

const removeReflectionFromGroup = async (reflectionId: string, {dataLoader}: GQLContext) => {
const pg = getKysely()
Expand Down Expand Up @@ -57,12 +56,23 @@ const removeReflectionFromGroup = async (reflectionId: string, {dataLoader}: GQL
.get('retroReflectionsByGroupId')
.load(oldReflectionGroupId)

const nextTitle = getGroupSmartTitle([reflection])
await updateSmartGroupTitle(reflectionGroupId, nextTitle)
const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId)
await updateGroupTitle({
reflections: [reflection],
reflectionGroupId: reflectionGroupId,
meetingId,
teamId: meeting.teamId,
dataLoader
})

if (oldReflections.length > 0) {
const oldTitle = getGroupSmartTitle(oldReflections)
await updateSmartGroupTitle(oldReflectionGroupId, oldTitle)
await updateGroupTitle({
reflections: oldReflections,
reflectionGroupId: oldReflectionGroupId,
meetingId,
teamId: meeting.teamId,
dataLoader
})
} else {
await pg
.updateTable('RetroReflectionGroup')
Expand Down
12 changes: 8 additions & 4 deletions packages/server/graphql/mutations/updateReflectionContent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {GraphQLID, GraphQLNonNull, GraphQLString} from 'graphql'
import {SubscriptionChannel} from 'parabol-client/types/constEnums'
import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString'
import isPhaseComplete from 'parabol-client/utils/meetings/isPhaseComplete'
import getGroupSmartTitle from 'parabol-client/utils/smartGroup/getGroupSmartTitle'
import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS'
import stringSimilarity from 'string-similarity'
import getKysely from '../../postgres/getKysely'
Expand All @@ -15,7 +14,7 @@ import UpdateReflectionContentPayload from '../types/UpdateReflectionContentPayl
import {getFeatureTier} from '../types/helpers/getFeatureTier'
import getReflectionEntities from './helpers/getReflectionEntities'
import getReflectionSentimentScore from './helpers/getReflectionSentimentScore'
import updateSmartGroupTitle from './helpers/updateReflectionLocation/updateSmartGroupTitle'
import updateGroupTitle from './helpers/updateGroupTitle'

export default {
type: UpdateReflectionContentPayload,
Expand Down Expand Up @@ -99,8 +98,13 @@ export default {
.get('retroReflectionsByGroupId')
.load(reflectionGroupId)

const newTitle = getGroupSmartTitle(reflectionsInGroup)
await updateSmartGroupTitle(reflectionGroupId, newTitle)
await updateGroupTitle({
reflections: reflectionsInGroup,
reflectionGroupId,
meetingId,
teamId,
dataLoader
})

const data = {meetingId, reflectionId}
publish(
Expand Down
34 changes: 34 additions & 0 deletions packages/server/utils/OpenAIServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,40 @@ class OpenAIServerManager {
return null
}
}

async generateGroupTitle(reflections: {plaintextContent: string}[]) {
if (!this.openAIApi) return null
const prompt = `Given these related retrospective comments, generate a short (2-4 words) theme or title that captures their essence. The title should be clear and actionable:
${reflections.map((r) => r.plaintextContent).join('\n')}
Return only the title, nothing else. Do not include quote marks around the title.`

try {
const response = await this.openAIApi.chat.completions.create({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'user',
content: prompt
}
],
temperature: 0.3,
max_tokens: 20,
top_p: 1,
frequency_penalty: 0,
presence_penalty: 0
})
const title =
(response.choices[0]?.message?.content?.trim() as string)?.replaceAll(/['"]/g, '') ?? null

return title
} catch (e) {
const error = e instanceof Error ? e : new Error('OpenAI failed to generate group title')
sendToSentry(error)
return null
}
}
}

export default OpenAIServerManager

0 comments on commit cfde723

Please sign in to comment.