From 8931593a43b2fa54a75e434495e4e6d6b91f5b54 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Thu, 28 Nov 2024 13:00:41 +0100 Subject: [PATCH 01/13] generate group titles with chatgpt --- .../ReflectionGroupTitleEditor.tsx | 3 +- .../addReflectionToGroup.ts | 47 +++++++++++++++++-- packages/server/utils/OpenAIServerManager.ts | 31 ++++++++++++ 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx b/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx index fc351007483..ec8c1e95981 100644 --- a/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx +++ b/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx @@ -127,6 +127,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) => { const title = e.target.value @@ -184,7 +185,7 @@ const ReflectionGroupTitleEditor = (props: Props) => { onBlur={onSubmit} onChange={onChange} onKeyPress={onKeyPress} - placeholder={RETRO_TOPIC_LABEL} + placeholder={isLoading ? '' : RETRO_TOPIC_LABEL} readOnly={readOnly} ref={titleInputRef} maxLength={200} diff --git a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts index bde8d49d943..9feb8f0541c 100644 --- a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts +++ b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts @@ -1,6 +1,8 @@ +import {SubscriptionChannel} from 'parabol-client/types/constEnums' import dndNoise from 'parabol-client/utils/dndNoise' -import getGroupSmartTitle from 'parabol-client/utils/smartGroup/getGroupSmartTitle' import getKysely from '../../../../postgres/getKysely' +import OpenAIServerManager from '../../../../utils/OpenAIServerManager' +import publish from '../../../../utils/publish' import {GQLContext} from './../../../graphql' import updateSmartGroupTitle from './updateSmartGroupTitle' @@ -51,18 +53,17 @@ const addReflectionToGroup = async ( reflection.updatedAt = now if (oldReflectionGroupId !== reflectionGroupId) { - // ths 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 nextTitle = smartTitle ?? '' 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}) @@ -72,9 +73,45 @@ const addReflectionToGroup = async ( await updateSmartGroupTitle(reflectionGroupId, nextTitle) } + // Fire off AI update without awaiting + const manager = new OpenAIServerManager() + manager.generateGroupTitle(nextReflections).then(async (aiTitle) => { + if (aiTitle) { + await updateSmartGroupTitle(reflectionGroupId, aiTitle) + publish( + SubscriptionChannel.MEETING, + meetingId, + 'UpdateReflectionGroupTitlePayload', + { + meetingId, + reflectionGroupId, + title: aiTitle + }, + {operationId: dataLoader.share()} + ) + } + }) + if (oldReflections.length > 0) { - const oldTitle = getGroupSmartTitle(oldReflections) + const oldTitle = '' await updateSmartGroupTitle(oldReflectionGroupId, oldTitle) + + manager.generateGroupTitle(oldReflections).then(async (aiTitle) => { + if (aiTitle) { + await updateSmartGroupTitle(oldReflectionGroupId, aiTitle) + publish( + SubscriptionChannel.MEETING, + meetingId, + 'UpdateReflectionGroupTitlePayload', + { + meetingId, + reflectionGroupId: oldReflectionGroupId, + title: aiTitle + }, + {operationId: dataLoader.share()} + ) + } + }) } else { await pg .updateTable('RetroReflectionGroup') diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 6143be1775c..815d2644ace 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -417,6 +417,37 @@ 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.` + + try { + const response = await this.openAIApi.chat.completions.create({ + model: 'gpt-4o-mini', + messages: [ + { + role: 'user', + content: prompt + } + ], + temperature: 0.3, + max_tokens: 20, + top_p: 1, + frequency_penalty: 0, + presence_penalty: 0 + }) + return (response.choices[0]?.message?.content?.trim() as string) ?? null + } catch (e) { + const error = e instanceof Error ? e : new Error('OpenAI failed to generate group title') + sendToSentry(error) + return null + } + } } export default OpenAIServerManager From 33f525e37121bc41f9a1ad5301dbcc70b267941e Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 2 Dec 2024 16:56:03 +0100 Subject: [PATCH 02/13] generate ai title --- .../EndDraggingReflectionMutation.ts | 20 +++++- .../mutations/helpers/generateAIGroupTitle.ts | 59 +++++++++++++++++ .../mutations/helpers/updateGroupTitle.ts | 36 ++++++++++ .../addReflectionToGroup.ts | 65 ++++++------------- packages/server/utils/OpenAIServerManager.ts | 13 +++- 5 files changed, 146 insertions(+), 47 deletions(-) create mode 100644 packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts create mode 100644 packages/server/graphql/mutations/helpers/updateGroupTitle.ts diff --git a/packages/client/mutations/EndDraggingReflectionMutation.ts b/packages/client/mutations/EndDraggingReflectionMutation.ts index eb9491b5619..2d9d942aba8 100644 --- a/packages/client/mutations/EndDraggingReflectionMutation.ts +++ b/packages/client/mutations/EndDraggingReflectionMutation.ts @@ -155,6 +155,18 @@ const EndDraggingReflectionMutation: SimpleMutation { @@ -170,6 +182,7 @@ const EndDraggingReflectionMutation: SimpleMutation const newReflectionGroupId = clientTempId() + // move a reflection into its own group if (!reflectionGroupId) { // create the new group @@ -179,7 +192,9 @@ const EndDraggingReflectionMutation: SimpleMutation { + const hasAIAccess = await canAccessAI(team, 'retrospective', dataLoader) + await updateSmartGroupTitle(reflectionGroupId, '') + + if (hasAIAccess) { + const manager = new OpenAIServerManager() + const aiTitle = await manager.generateGroupTitle(reflections) + if (aiTitle) { + await updateSmartGroupTitle(reflectionGroupId, aiTitle) + publish( + SubscriptionChannel.MEETING, + meetingId, + 'UpdateReflectionGroupTitlePayload', + { + meetingId, + reflectionGroupId, + title: aiTitle + }, + {operationId: dataLoader.share()} + ) + } + } else { + const smartTitle = getGroupSmartTitle(reflections) + await updateSmartGroupTitle(reflectionGroupId, smartTitle) + publish( + SubscriptionChannel.MEETING, + meetingId, + 'UpdateReflectionGroupTitlePayload', + { + meetingId, + reflectionGroupId, + title: smartTitle + }, + {operationId: dataLoader.share()} + ) + } +} + +export default generateAIGroupTitle diff --git a/packages/server/graphql/mutations/helpers/updateGroupTitle.ts b/packages/server/graphql/mutations/helpers/updateGroupTitle.ts new file mode 100644 index 00000000000..2e5a4143cd5 --- /dev/null +++ b/packages/server/graphql/mutations/helpers/updateGroupTitle.ts @@ -0,0 +1,36 @@ +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 + 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, team, dataLoader) + } +} + +export default updateGroupTitle diff --git a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts index 9feb8f0541c..24fda8f2ece 100644 --- a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts +++ b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts @@ -1,10 +1,7 @@ -import {SubscriptionChannel} from 'parabol-client/types/constEnums' import dndNoise from 'parabol-client/utils/dndNoise' import getKysely from '../../../../postgres/getKysely' -import OpenAIServerManager from '../../../../utils/OpenAIServerManager' -import publish from '../../../../utils/publish' +import updateGroupTitle from '../updateGroupTitle' import {GQLContext} from './../../../graphql' -import updateSmartGroupTitle from './updateSmartGroupTitle' const addReflectionToGroup = async ( reflectionId: string, @@ -53,64 +50,44 @@ const addReflectionToGroup = async ( reflection.updatedAt = now if (oldReflectionGroupId !== reflectionGroupId) { + // 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 ?? '' 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) + // Get team for AI access check + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) + await updateGroupTitle({ + reflections: nextReflections, + reflectionGroupId, + meetingId, + teamId: meeting.teamId, + dataLoader + }) } - // Fire off AI update without awaiting - const manager = new OpenAIServerManager() - manager.generateGroupTitle(nextReflections).then(async (aiTitle) => { - if (aiTitle) { - await updateSmartGroupTitle(reflectionGroupId, aiTitle) - publish( - SubscriptionChannel.MEETING, - meetingId, - 'UpdateReflectionGroupTitlePayload', - { - meetingId, - reflectionGroupId, - title: aiTitle - }, - {operationId: dataLoader.share()} - ) - } - }) - if (oldReflections.length > 0) { - const oldTitle = '' - await updateSmartGroupTitle(oldReflectionGroupId, oldTitle) - - manager.generateGroupTitle(oldReflections).then(async (aiTitle) => { - if (aiTitle) { - await updateSmartGroupTitle(oldReflectionGroupId, aiTitle) - publish( - SubscriptionChannel.MEETING, - meetingId, - 'UpdateReflectionGroupTitlePayload', - { - meetingId, - reflectionGroupId: oldReflectionGroupId, - title: aiTitle - }, - {operationId: dataLoader.share()} - ) - } + // Get team for AI access check (reuse from above if possible) + const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) + await updateGroupTitle({ + reflections: oldReflections, + reflectionGroupId: oldReflectionGroupId, + meetingId, + teamId: meeting.teamId, + dataLoader }) } else { await pg diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 815d2644ace..e4511fd8120 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -420,15 +420,19 @@ class OpenAIServerManager { async generateGroupTitle(reflections: {plaintextContent: string}[]) { if (!this.openAIApi) return null + const startTime = Date.now() + console.log('🕒 Starting AI title generation...') + 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.` +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-4o-mini', + // model: 'gpt-4o-mini', + model: 'gpt-3.5-turbo', messages: [ { role: 'user', @@ -441,8 +445,13 @@ Return only the title, nothing else.` frequency_penalty: 0, presence_penalty: 0 }) + const duration = Date.now() - startTime + console.log(`✨ AI title generated in ${duration}ms:`, response.choices[0]?.message.content) + return (response.choices[0]?.message?.content?.trim() as string) ?? null } catch (e) { + const duration = Date.now() - startTime + console.log(`❌ AI title generation failed after ${duration}ms`) const error = e instanceof Error ? e : new Error('OpenAI failed to generate group title') sendToSentry(error) return null From 6334abd92fb7245cd28d9b4f0934c448cc3ef106 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 3 Dec 2024 16:38:23 +0100 Subject: [PATCH 03/13] fix race confition --- .../EndDraggingReflectionMutation.ts | 12 ---------- .../UpdateReflectionContentMutation.ts | 1 + .../mutations/helpers/generateAIGroupTitle.ts | 3 ++- .../mutations/helpers/updateGroupTitle.ts | 7 +++++- .../removeReflectionFromGroup.ts | 22 ++++++++++++++----- .../mutations/updateReflectionContent.ts | 14 ++++++++---- .../mutations/updateReflectionGroupTitle.ts | 2 ++ packages/server/utils/OpenAIServerManager.ts | 12 +++------- 8 files changed, 40 insertions(+), 33 deletions(-) diff --git a/packages/client/mutations/EndDraggingReflectionMutation.ts b/packages/client/mutations/EndDraggingReflectionMutation.ts index 2d9d942aba8..61413787364 100644 --- a/packages/client/mutations/EndDraggingReflectionMutation.ts +++ b/packages/client/mutations/EndDraggingReflectionMutation.ts @@ -155,18 +155,6 @@ const EndDraggingReflectionMutation: SimpleMutation { diff --git a/packages/client/mutations/UpdateReflectionContentMutation.ts b/packages/client/mutations/UpdateReflectionContentMutation.ts index 6905ae3ca67..886ce372f6f 100644 --- a/packages/client/mutations/UpdateReflectionContentMutation.ts +++ b/packages/client/mutations/UpdateReflectionContentMutation.ts @@ -18,6 +18,7 @@ graphql` content retroReflectionGroup { title + smartTitle } } } diff --git a/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts b/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts index d736fa92d4a..6f8110d9e0b 100644 --- a/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts +++ b/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts @@ -20,13 +20,13 @@ const generateAIGroupTitle = async ( dataLoader: DataLoaderWorker ) => { const hasAIAccess = await canAccessAI(team, 'retrospective', dataLoader) - await updateSmartGroupTitle(reflectionGroupId, '') if (hasAIAccess) { const manager = new OpenAIServerManager() const aiTitle = await manager.generateGroupTitle(reflections) if (aiTitle) { await updateSmartGroupTitle(reflectionGroupId, aiTitle) + dataLoader.get('retroReflectionGroups').clear(reflectionGroupId) publish( SubscriptionChannel.MEETING, meetingId, @@ -42,6 +42,7 @@ const generateAIGroupTitle = async ( } else { const smartTitle = getGroupSmartTitle(reflections) await updateSmartGroupTitle(reflectionGroupId, smartTitle) + dataLoader.get('retroReflectionGroups').clear(reflectionGroupId) publish( SubscriptionChannel.MEETING, meetingId, diff --git a/packages/server/graphql/mutations/helpers/updateGroupTitle.ts b/packages/server/graphql/mutations/helpers/updateGroupTitle.ts index 2e5a4143cd5..73e3b8dc775 100644 --- a/packages/server/graphql/mutations/helpers/updateGroupTitle.ts +++ b/packages/server/graphql/mutations/helpers/updateGroupTitle.ts @@ -18,7 +18,12 @@ const updateGroupTitle = async (input: Input) => { const {reflections, reflectionGroupId, meetingId, teamId, dataLoader} = input const team = await dataLoader.get('teams').loadNonNull(teamId) const hasAIAccess = await canAccessAI(team, 'retrospective', dataLoader) - if (!hasAIAccess) { + + if (reflections.length === 1) { + // For single reflection, use its content as the title + const newTitle = reflections[0].plaintextContent + await updateSmartGroupTitle(reflectionGroupId, newTitle) + } else if (!hasAIAccess) { const smartTitle = getGroupSmartTitle(reflections) await updateSmartGroupTitle(reflectionGroupId, smartTitle) } else { diff --git a/packages/server/graphql/mutations/helpers/updateReflectionLocation/removeReflectionFromGroup.ts b/packages/server/graphql/mutations/helpers/updateReflectionLocation/removeReflectionFromGroup.ts index d82fb1c99f0..02fc3876c54 100644 --- a/packages/server/graphql/mutations/helpers/updateReflectionLocation/removeReflectionFromGroup.ts +++ b/packages/server/graphql/mutations/helpers/updateReflectionLocation/removeReflectionFromGroup.ts @@ -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() @@ -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') diff --git a/packages/server/graphql/mutations/updateReflectionContent.ts b/packages/server/graphql/mutations/updateReflectionContent.ts index 180a101d0a0..dbe473dfd96 100644 --- a/packages/server/graphql/mutations/updateReflectionContent.ts +++ b/packages/server/graphql/mutations/updateReflectionContent.ts @@ -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' @@ -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, @@ -99,8 +98,15 @@ export default { .get('retroReflectionsByGroupId') .load(reflectionGroupId) - const newTitle = getGroupSmartTitle(reflectionsInGroup) - await updateSmartGroupTitle(reflectionGroupId, newTitle) + // const newTitle = getGroupSmartTitle(reflectionsInGroup) + // await updateSmartGroupTitle(reflectionGroupId, newTitle) + await updateGroupTitle({ + reflections: reflectionsInGroup, + reflectionGroupId, + meetingId, + teamId, + dataLoader + }) const data = {meetingId, reflectionId} publish( diff --git a/packages/server/graphql/mutations/updateReflectionGroupTitle.ts b/packages/server/graphql/mutations/updateReflectionGroupTitle.ts index 7470bddbbf1..a508f8070a0 100644 --- a/packages/server/graphql/mutations/updateReflectionGroupTitle.ts +++ b/packages/server/graphql/mutations/updateReflectionGroupTitle.ts @@ -90,6 +90,8 @@ export default { } const data = {meetingId, reflectionGroupId} + + console.log('here <><><><>><><>><>><>><><') publish( SubscriptionChannel.MEETING, meetingId, diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index e4511fd8120..8b880bae1dd 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -420,9 +420,6 @@ class OpenAIServerManager { async generateGroupTitle(reflections: {plaintextContent: string}[]) { if (!this.openAIApi) return null - const startTime = Date.now() - console.log('🕒 Starting AI title generation...') - 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')} @@ -431,7 +428,6 @@ 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-4o-mini', model: 'gpt-3.5-turbo', messages: [ { @@ -445,13 +441,11 @@ Return only the title, nothing else. Do not include quote marks around the title frequency_penalty: 0, presence_penalty: 0 }) - const duration = Date.now() - startTime - console.log(`✨ AI title generated in ${duration}ms:`, response.choices[0]?.message.content) + const title = + (response.choices[0]?.message?.content?.trim() as string)?.replaceAll(/['"]/g, '') ?? null - return (response.choices[0]?.message?.content?.trim() as string) ?? null + return title } catch (e) { - const duration = Date.now() - startTime - console.log(`❌ AI title generation failed after ${duration}ms`) const error = e instanceof Error ? e : new Error('OpenAI failed to generate group title') sendToSentry(error) return null From 391916b52df93fadcfc58af6fd0d2cc9ff7947cf Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 3 Dec 2024 16:51:43 +0100 Subject: [PATCH 04/13] clean up --- packages/client/mutations/EndDraggingReflectionMutation.ts | 5 ++--- packages/client/mutations/UpdateReflectionContentMutation.ts | 1 - .../helpers/updateReflectionLocation/addReflectionToGroup.ts | 1 - packages/server/graphql/mutations/updateReflectionContent.ts | 2 -- .../server/graphql/mutations/updateReflectionGroupTitle.ts | 1 - 5 files changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/client/mutations/EndDraggingReflectionMutation.ts b/packages/client/mutations/EndDraggingReflectionMutation.ts index 61413787364..541ff78ce6d 100644 --- a/packages/client/mutations/EndDraggingReflectionMutation.ts +++ b/packages/client/mutations/EndDraggingReflectionMutation.ts @@ -181,8 +181,8 @@ const EndDraggingReflectionMutation: SimpleMutation<><><>><><>><>><>><><') publish( SubscriptionChannel.MEETING, meetingId, From 3a3e56b8cfb0a8819ddc903950b1192b28d42be5 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 4 Dec 2024 16:20:45 +0100 Subject: [PATCH 05/13] show elipsis when loading --- .../ReflectionGroupTitleEditor.tsx | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx b/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx index ec8c1e95981..46fd8a0af27 100644 --- a/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx +++ b/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx @@ -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 { @@ -179,19 +180,30 @@ const ReflectionGroupTitleEditor = (props: Props) => { - + {isLoading ? ( + + + + ) : ( + + )} {error && {error.message}} From 1044c0d55fe81f600c403432763fe599a48eb427 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 4 Dec 2024 16:36:54 +0100 Subject: [PATCH 06/13] remove redundant ai check --- .../mutations/helpers/generateAIGroupTitle.ts | 58 ++++++------------- .../mutations/helpers/updateGroupTitle.ts | 11 ++-- .../addReflectionToGroup.ts | 1 - .../mutations/updateReflectionGroupTitle.ts | 1 - 4 files changed, 25 insertions(+), 46 deletions(-) diff --git a/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts b/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts index 6f8110d9e0b..eae24996321 100644 --- a/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts +++ b/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts @@ -1,10 +1,8 @@ import {SubscriptionChannel} from 'parabol-client/types/constEnums' -import getGroupSmartTitle from '../../../../client/utils/smartGroup/getGroupSmartTitle' -import {Team} from '../../../postgres/types' import OpenAIServerManager from '../../../utils/OpenAIServerManager' import publish from '../../../utils/publish' +import standardError from '../../../utils/standardError' import {DataLoaderWorker} from '../../graphql' -import canAccessAI from './canAccessAI' import updateSmartGroupTitle from './updateReflectionLocation/updateSmartGroupTitle' interface Reflection { @@ -16,45 +14,27 @@ const generateAIGroupTitle = async ( reflections: Reflection[], reflectionGroupId: string, meetingId: string, - team: Team, dataLoader: DataLoaderWorker ) => { - const hasAIAccess = await canAccessAI(team, 'retrospective', dataLoader) - - if (hasAIAccess) { - const manager = new OpenAIServerManager() - const aiTitle = await manager.generateGroupTitle(reflections) - if (aiTitle) { - await updateSmartGroupTitle(reflectionGroupId, aiTitle) - dataLoader.get('retroReflectionGroups').clear(reflectionGroupId) - publish( - SubscriptionChannel.MEETING, - meetingId, - 'UpdateReflectionGroupTitlePayload', - { - meetingId, - reflectionGroupId, - title: aiTitle - }, - {operationId: dataLoader.share()} - ) - } - } else { - const smartTitle = getGroupSmartTitle(reflections) - await updateSmartGroupTitle(reflectionGroupId, smartTitle) - dataLoader.get('retroReflectionGroups').clear(reflectionGroupId) - publish( - SubscriptionChannel.MEETING, - meetingId, - 'UpdateReflectionGroupTitlePayload', - { - meetingId, - reflectionGroupId, - title: smartTitle - }, - {operationId: dataLoader.share()} - ) + const manager = new OpenAIServerManager() + const aiTitle = await manager.generateGroupTitle(reflections) + if (!aiTitle) { + standardError(new Error('Failed to generate AI title')) + return } + await updateSmartGroupTitle(reflectionGroupId, aiTitle) + dataLoader.get('retroReflectionGroups').clear(reflectionGroupId) + publish( + SubscriptionChannel.MEETING, + meetingId, + 'UpdateReflectionGroupTitlePayload', + { + meetingId, + reflectionGroupId, + title: aiTitle + }, + {operationId: dataLoader.share()} + ) } export default generateAIGroupTitle diff --git a/packages/server/graphql/mutations/helpers/updateGroupTitle.ts b/packages/server/graphql/mutations/helpers/updateGroupTitle.ts index 73e3b8dc775..658c8b84569 100644 --- a/packages/server/graphql/mutations/helpers/updateGroupTitle.ts +++ b/packages/server/graphql/mutations/helpers/updateGroupTitle.ts @@ -16,14 +16,15 @@ type Input = { const updateGroupTitle = async (input: Input) => { const {reflections, reflectionGroupId, meetingId, teamId, dataLoader} = input - const team = await dataLoader.get('teams').loadNonNull(teamId) - const hasAIAccess = await canAccessAI(team, 'retrospective', dataLoader) - if (reflections.length === 1) { // For single reflection, use its content as the title const newTitle = reflections[0].plaintextContent await updateSmartGroupTitle(reflectionGroupId, newTitle) - } else if (!hasAIAccess) { + 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 { @@ -34,7 +35,7 @@ const updateGroupTitle = async (input: Input) => { .where('id', '=', reflectionGroupId) .execute() // Generate title and don't await or the reflection will hang when it's dropped - generateAIGroupTitle(reflections, reflectionGroupId, meetingId, team, dataLoader) + generateAIGroupTitle(reflections, reflectionGroupId, meetingId, dataLoader) } } diff --git a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts index 259080ce153..29f5df88c5c 100644 --- a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts +++ b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts @@ -79,7 +79,6 @@ const addReflectionToGroup = async ( } if (oldReflections.length > 0) { - // Get team for AI access check (reuse from above if possible) const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) await updateGroupTitle({ reflections: oldReflections, diff --git a/packages/server/graphql/mutations/updateReflectionGroupTitle.ts b/packages/server/graphql/mutations/updateReflectionGroupTitle.ts index a2a7a1f91ea..7470bddbbf1 100644 --- a/packages/server/graphql/mutations/updateReflectionGroupTitle.ts +++ b/packages/server/graphql/mutations/updateReflectionGroupTitle.ts @@ -90,7 +90,6 @@ export default { } const data = {meetingId, reflectionGroupId} - publish( SubscriptionChannel.MEETING, meetingId, From b073840c94a3a5e9148ac74b1a4f7e23a3d557f1 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 4 Dec 2024 16:50:44 +0100 Subject: [PATCH 07/13] simplify elipsis span --- .../ReflectionGroup/ReflectionGroupTitleEditor.tsx | 7 +------ .../graphql/mutations/helpers/generateAIGroupTitle.ts | 8 +++----- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx b/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx index 46fd8a0af27..c2927fb7536 100644 --- a/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx +++ b/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx @@ -181,12 +181,7 @@ const ReflectionGroupTitleEditor = (props: Props) => { {isLoading ? ( - + ) : ( diff --git a/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts b/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts index eae24996321..338221da6b5 100644 --- a/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts +++ b/packages/server/graphql/mutations/helpers/generateAIGroupTitle.ts @@ -18,11 +18,9 @@ const generateAIGroupTitle = async ( ) => { const manager = new OpenAIServerManager() const aiTitle = await manager.generateGroupTitle(reflections) - if (!aiTitle) { - standardError(new Error('Failed to generate AI title')) - return - } - await updateSmartGroupTitle(reflectionGroupId, aiTitle) + const newTitle = aiTitle ?? reflections[0]?.plaintextContent ?? '' + if (!aiTitle) standardError(new Error('Failed to generate AI title')) + await updateSmartGroupTitle(reflectionGroupId, newTitle) dataLoader.get('retroReflectionGroups').clear(reflectionGroupId) publish( SubscriptionChannel.MEETING, From ecd3f1be136550bc72500c2735db9ab0f693003a Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Mon, 9 Dec 2024 17:34:07 +0000 Subject: [PATCH 08/13] update suggest group titles --- .../client/mutations/AutogroupMutation.ts | 1 + .../mutations/helpers/updateGroupTitle.ts | 1 + .../addReflectionToGroup.ts | 24 +++++++------------ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/client/mutations/AutogroupMutation.ts b/packages/client/mutations/AutogroupMutation.ts index 8f05c4d042c..3a66969a2e1 100644 --- a/packages/client/mutations/AutogroupMutation.ts +++ b/packages/client/mutations/AutogroupMutation.ts @@ -13,6 +13,7 @@ graphql` reflectionGroups { id title + smartTitle reflections { id plaintextContent diff --git a/packages/server/graphql/mutations/helpers/updateGroupTitle.ts b/packages/server/graphql/mutations/helpers/updateGroupTitle.ts index 658c8b84569..3ebe85bdf3d 100644 --- a/packages/server/graphql/mutations/helpers/updateGroupTitle.ts +++ b/packages/server/graphql/mutations/helpers/updateGroupTitle.ts @@ -15,6 +15,7 @@ type Input = { } const updateGroupTitle = async (input: Input) => { + console.log('in update group...') const {reflections, reflectionGroupId, meetingId, teamId, dataLoader} = input if (reflections.length === 1) { // For single reflection, use its content as the title diff --git a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts index 29f5df88c5c..672e287ccd2 100644 --- a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts +++ b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts @@ -2,6 +2,7 @@ import dndNoise from 'parabol-client/utils/dndNoise' import getKysely from '../../../../postgres/getKysely' import updateGroupTitle from '../updateGroupTitle' import {GQLContext} from './../../../graphql' +import updateSmartGroupTitle from './updateSmartGroupTitle' const addReflectionToGroup = async ( reflectionId: string, @@ -14,10 +15,9 @@ const addReflectionToGroup = async ( const reflection = await dataLoader.get('retroReflections').load(reflectionId) if (!reflection) throw new Error('Reflection not found') const {reflectionGroupId: oldReflectionGroupId, meetingId: reflectionMeetingId} = reflection - const [reflectionGroup, oldReflectionGroup] = await Promise.all([ - dataLoader.get('retroReflectionGroups').loadNonNull(reflectionGroupId), - dataLoader.get('retroReflectionGroups').loadNonNull(oldReflectionGroupId) - ]) + const reflectionGroup = await dataLoader + .get('retroReflectionGroups') + .loadNonNull(reflectionGroupId) dataLoader.get('retroReflectionGroups').clear(reflectionGroupId) dataLoader.get('retroReflectionGroups').clear(oldReflectionGroupId) @@ -56,17 +56,11 @@ const addReflectionToGroup = async ( .get('retroReflectionsByGroupId') .load(oldReflectionGroupId) - 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: smartTitle ?? ''}) - .where('id', '=', reflectionGroupId) - .execute() + if (smartTitle) { + // smartTitle exists when autogrouping or resetting groups + await updateSmartGroupTitle(reflectionGroupId, smartTitle) + reflectionGroup.smartTitle = smartTitle + reflectionGroup.title = smartTitle } else { const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) await updateGroupTitle({ From b98b2d7e9384b9c8b4125900fd6994c339935987 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Wed, 11 Dec 2024 16:21:20 +0000 Subject: [PATCH 09/13] dont show loading state after removing title --- .../components/ReflectionGroup/ReflectionGroupTitleEditor.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx b/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx index c2927fb7536..9ff333051ca 100644 --- a/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx +++ b/packages/client/components/ReflectionGroup/ReflectionGroupTitleEditor.tsx @@ -128,9 +128,11 @@ const ReflectionGroupTitleEditor = (props: Props) => { const {id: reflectionGroupId, title} = reflectionGroup const dirtyRef = useRef(false) const initialTitleRef = useRef(title) - const isLoading = title === '' + + const isLoading = title === '' && !dirtyRef.current const onChange = (e: React.ChangeEvent) => { + dirtyRef.current = true const title = e.target.value commitLocalUpdate(atmosphere, (store) => { const reflectionGroup = store.get(reflectionGroupId) From 861753688ad2f938d80cbf93c0e0bc7ee32aedbe Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 17 Dec 2024 17:16:48 +0000 Subject: [PATCH 10/13] update prompt and handle custom group edge case --- .../addReflectionToGroup.ts | 20 +++++++++++++++---- packages/server/utils/OpenAIServerManager.ts | 8 +++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts index 672e287ccd2..222679d1677 100644 --- a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts +++ b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts @@ -15,9 +15,10 @@ const addReflectionToGroup = async ( const reflection = await dataLoader.get('retroReflections').load(reflectionId) if (!reflection) throw new Error('Reflection not found') const {reflectionGroupId: oldReflectionGroupId, meetingId: reflectionMeetingId} = reflection - const reflectionGroup = await dataLoader - .get('retroReflectionGroups') - .loadNonNull(reflectionGroupId) + const [reflectionGroup, oldReflectionGroup] = await Promise.all([ + dataLoader.get('retroReflectionGroups').loadNonNull(reflectionGroupId), + dataLoader.get('retroReflectionGroups').loadNonNull(oldReflectionGroupId) + ]) dataLoader.get('retroReflectionGroups').clear(reflectionGroupId) dataLoader.get('retroReflectionGroups').clear(oldReflectionGroupId) @@ -56,7 +57,18 @@ const addReflectionToGroup = async ( .get('retroReflectionsByGroupId') .load(oldReflectionGroupId) - if (smartTitle) { + 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: smartTitle ?? ''}) + .where('id', '=', reflectionGroupId) + .execute() + } else if (smartTitle) { // smartTitle exists when autogrouping or resetting groups await updateSmartGroupTitle(reflectionGroupId, smartTitle) reflectionGroup.smartTitle = smartTitle diff --git a/packages/server/utils/OpenAIServerManager.ts b/packages/server/utils/OpenAIServerManager.ts index 8b880bae1dd..29a7b315cad 100644 --- a/packages/server/utils/OpenAIServerManager.ts +++ b/packages/server/utils/OpenAIServerManager.ts @@ -420,11 +420,11 @@ class OpenAIServerManager { 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: + const prompt = `Generate a short (2-4 words) theme or title that captures the essence of these related retrospective comments. 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.` +Important: Respond with ONLY the title itself. Do not include any prefixes like "Title:" or any quote marks. Do not provide any additional explanation.` try { const response = await this.openAIApi.chat.completions.create({ @@ -442,7 +442,9 @@ Return only the title, nothing else. Do not include quote marks around the title presence_penalty: 0 }) const title = - (response.choices[0]?.message?.content?.trim() as string)?.replaceAll(/['"]/g, '') ?? null + (response.choices[0]?.message?.content?.trim() as string) + ?.replace(/^[Tt]itle:*\s*/gi, '') // Remove "Title:" prefix + ?.replaceAll(/['"]/g, '') ?? null return title } catch (e) { From 833c8fa8ae72bea7270cc9687c92f59ff8858668 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 17 Dec 2024 17:42:26 +0000 Subject: [PATCH 11/13] feat: remove suggest groups --- packages/client/components/RetroGroupPhase.tsx | 6 +++--- .../components/OrgBilling/OrgFeatureFlags.tsx | 3 +-- .../mutations/EndRetrospectiveMutation.ts | 5 ----- .../graphql/mutations/helpers/generateGroups.ts | 6 ++---- ...12-17T17:34:13.338Z_remove-suggest-groups.ts | 17 +++++++++++++++++ 5 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 packages/server/postgres/migrations/2024-12-17T17:34:13.338Z_remove-suggest-groups.ts diff --git a/packages/client/components/RetroGroupPhase.tsx b/packages/client/components/RetroGroupPhase.tsx index 41dc731367f..977ec937e46 100644 --- a/packages/client/components/RetroGroupPhase.tsx +++ b/packages/client/components/RetroGroupPhase.tsx @@ -63,7 +63,7 @@ const RetroGroupPhase = (props: Props) => { } organization { tier - hasSuggestGroupsFlag: featureFlag(featureName: "suggestGroups") + useAI } } `, @@ -80,7 +80,7 @@ const RetroGroupPhase = (props: Props) => { autogroupReflectionGroups, resetReflectionGroups } = meeting - const {hasSuggestGroupsFlag, tier} = organization + const {useAI, tier} = organization const {openTooltip, closeTooltip, tooltipPortal, originRef} = useTooltip( MenuPosition.UPPER_CENTER ) @@ -114,7 +114,7 @@ const RetroGroupPhase = (props: Props) => { {'Drag cards to group by common topics'} - {hasSuggestGroupsFlag && + {useAI && (showSuggestGroups ? ( = { insights: 'Team Insights', publicTeams: 'Public Teams', relatedDiscussions: 'Related Discussions', - standupAISummary: 'Standup AI Summary', - suggestGroups: 'AI Reflection Group Suggestions' + standupAISummary: 'Standup AI Summary' } interface Props { diff --git a/packages/client/mutations/EndRetrospectiveMutation.ts b/packages/client/mutations/EndRetrospectiveMutation.ts index 41b8b1387d8..dcd40f7a125 100644 --- a/packages/client/mutations/EndRetrospectiveMutation.ts +++ b/packages/client/mutations/EndRetrospectiveMutation.ts @@ -124,11 +124,6 @@ export const endRetrospectiveTeamOnNext: OnNextHandler< const hasTeamHealth = phases.some((phase) => phase.phaseType === 'TEAM_HEALTH') const pathname = `/new-summary/${meetingId}` const search = new URLSearchParams() - const hasSuggestGroups = !!autogroupReflectionGroups?.length - if (hasSuggestGroups) { - const suggestGroupsStr = reflections.length > 40 ? 'sg-xl' : 'sg' - search.append(suggestGroupsStr, 'true') - } if (hasOpenAISummary) { search.append('ai', 'true') } diff --git a/packages/server/graphql/mutations/helpers/generateGroups.ts b/packages/server/graphql/mutations/helpers/generateGroups.ts index 0f2b1264926..639ef248a59 100644 --- a/packages/server/graphql/mutations/helpers/generateGroups.ts +++ b/packages/server/graphql/mutations/helpers/generateGroups.ts @@ -6,6 +6,7 @@ import OpenAIServerManager from '../../../utils/OpenAIServerManager' import {analytics} from '../../../utils/analytics/analytics' import publish from '../../../utils/publish' import {DataLoaderWorker} from '../../graphql' +import canAccessAI from './canAccessAI' const generateGroups = async ( reflections: RetroReflection[], @@ -15,10 +16,7 @@ const generateGroups = async ( if (reflections.length === 0) return const {meetingId} = reflections[0]! const team = await dataLoader.get('teams').loadNonNull(teamId) - const hasSuggestGroupsFlag = await dataLoader - .get('featureFlagByOwnerId') - .load({ownerId: team.orgId, featureName: 'suggestGroups'}) - if (!hasSuggestGroupsFlag) return + if (!(await canAccessAI(team, 'retrospective', dataLoader))) return const groupReflectionsInput = reflections.map((reflection) => reflection.plaintextContent) const manager = new OpenAIServerManager() diff --git a/packages/server/postgres/migrations/2024-12-17T17:34:13.338Z_remove-suggest-groups.ts b/packages/server/postgres/migrations/2024-12-17T17:34:13.338Z_remove-suggest-groups.ts new file mode 100644 index 00000000000..7576f57ad8a --- /dev/null +++ b/packages/server/postgres/migrations/2024-12-17T17:34:13.338Z_remove-suggest-groups.ts @@ -0,0 +1,17 @@ +import type {Kysely} from 'kysely' + +export async function up(db: Kysely): Promise { + await db.deleteFrom('FeatureFlag').where('featureName', '=', 'suggestGroups').execute() +} + +export async function down(db: Kysely): Promise { + await db + .insertInto('FeatureFlag') + .values({ + featureName: 'suggestGroups', + description: 'Auto-group reflections using AI', + expiresAt: new Date('2025-01-31T00:00:00.000Z'), + scope: 'Organization' + }) + .execute() +} From 2e4ef5212fc8b0adea0976c749dbf226000af079 Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 17 Dec 2024 18:51:58 +0000 Subject: [PATCH 12/13] remove suggest groups flag --- .../helpers/updateReflectionLocation/addReflectionToGroup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts index 222679d1677..96bdff99d7a 100644 --- a/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts +++ b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts @@ -14,6 +14,7 @@ const addReflectionToGroup = async ( const now = new Date() const reflection = await dataLoader.get('retroReflections').load(reflectionId) if (!reflection) throw new Error('Reflection not found') + const {reflectionGroupId: oldReflectionGroupId, meetingId: reflectionMeetingId} = reflection const [reflectionGroup, oldReflectionGroup] = await Promise.all([ dataLoader.get('retroReflectionGroups').loadNonNull(reflectionGroupId), From aad02649ab6de90deb9ecfad9a71d2e8ab18e64f Mon Sep 17 00:00:00 2001 From: Nick O'Ferrall Date: Tue, 17 Dec 2024 18:56:19 +0000 Subject: [PATCH 13/13] remove unused autogroupReflectionGroups --- .../client/mutations/EndRetrospectiveMutation.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/client/mutations/EndRetrospectiveMutation.ts b/packages/client/mutations/EndRetrospectiveMutation.ts index dcd40f7a125..1d3f912fa7d 100644 --- a/packages/client/mutations/EndRetrospectiveMutation.ts +++ b/packages/client/mutations/EndRetrospectiveMutation.ts @@ -28,9 +28,6 @@ graphql` reflectionCount taskCount topicCount - autogroupReflectionGroups { - groupTitle - } organization { useAI } @@ -97,14 +94,7 @@ export const endRetrospectiveTeamOnNext: OnNextHandler< const {isKill, meeting} = payload const {atmosphere, history} = context if (!meeting) return - const { - id: meetingId, - teamId, - reflectionGroups, - phases, - autogroupReflectionGroups, - organization - } = meeting + const {id: meetingId, teamId, reflectionGroups, phases, organization} = meeting if (meetingId === RetroDemo.MEETING_ID) { if (isKill) { window.localStorage.removeItem('retroDemo')