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) 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/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/client/mutations/EndRetrospectiveMutation.ts b/packages/client/mutations/EndRetrospectiveMutation.ts index 41b8b1387d8..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') @@ -124,11 +114,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/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts b/packages/server/graphql/mutations/helpers/updateReflectionLocation/addReflectionToGroup.ts index 29f5df88c5c..96bdff99d7a 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, @@ -13,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), @@ -67,6 +69,11 @@ const addReflectionToGroup = async ( .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 + reflectionGroup.title = smartTitle } else { const meeting = await dataLoader.get('newMeetings').loadNonNull(meetingId) await updateGroupTitle({ 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() +} 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) {