From 97879b8d6ae811f6f4dac6944fe35fdd1f1e43e9 Mon Sep 17 00:00:00 2001 From: Carlos Cano Date: Fri, 6 Dec 2024 10:37:07 +0100 Subject: [PATCH] Merge pull request #7188 from alkem-io/alkemio-1456/async-guidance Async Chat Guidance engine using Matrix rooms instead of direct querying --- src/core/apollo/generated/apollo-helpers.ts | 32 +-- src/core/apollo/generated/apollo-hooks.ts | 211 ++++++++++++--- src/core/apollo/generated/graphql-schema.ts | 241 ++++++++++++------ .../PostDashboardContainer.tsx | 2 +- .../discussion/pages/DiscussionPage.tsx | 2 +- .../discussion/views/DiscussionView.tsx | 2 +- .../room/Comments/MessageView.tsx | 2 +- .../room/Comments/useMessages.ts | 2 +- .../room/Comments/useRestoredMessages.ts | 2 +- .../communication/room/models/Message.ts | 2 +- .../calendar/CalendarEventDetailContainer.tsx | 2 +- src/main/guidance/chatWidget/ChatWidget.tsx | 117 ++++++--- .../chatWidget/ChatWidgetMutations.graphql | 16 +- .../chatWidget/ChatWidgetQueries.graphql | 23 +- .../guidance/chatWidget/ChatWidgetStyles.tsx | 3 +- .../formatChatGuidanceResponseAsMarkdown.ts | 38 --- .../useChatGuidanceCommunication.ts | 111 ++++++++ src/root.tsx | 3 - 18 files changed, 587 insertions(+), 224 deletions(-) delete mode 100644 src/main/guidance/chatWidget/formatChatGuidanceResponseAsMarkdown.ts create mode 100644 src/main/guidance/chatWidget/useChatGuidanceCommunication.ts diff --git a/src/core/apollo/generated/apollo-helpers.ts b/src/core/apollo/generated/apollo-helpers.ts index bca044d25c..0590fa06f6 100644 --- a/src/core/apollo/generated/apollo-helpers.ts +++ b/src/core/apollo/generated/apollo-helpers.ts @@ -556,7 +556,6 @@ export type AiPersonaServiceFieldPolicy = { export type AiServerKeySpecifier = ( | 'aiPersonaService' | 'aiPersonaServices' - | 'askAiPersonaServiceQuestion' | 'authorization' | 'createdDate' | 'defaultAiPersonaService' @@ -567,7 +566,6 @@ export type AiServerKeySpecifier = ( export type AiServerFieldPolicy = { aiPersonaService?: FieldPolicy | FieldReadFunction; aiPersonaServices?: FieldPolicy | FieldReadFunction; - askAiPersonaServiceQuestion?: FieldPolicy | FieldReadFunction; authorization?: FieldPolicy | FieldReadFunction; createdDate?: FieldPolicy | FieldReadFunction; defaultAiPersonaService?: FieldPolicy | FieldReadFunction; @@ -1922,22 +1920,17 @@ export type MessageFieldPolicy = { timestamp?: FieldPolicy | FieldReadFunction; }; export type MessageAnswerQuestionKeySpecifier = ( - | 'answer' + | 'error' | 'id' | 'question' - | 'sources' + | 'success' | MessageAnswerQuestionKeySpecifier )[]; export type MessageAnswerQuestionFieldPolicy = { - answer?: FieldPolicy | FieldReadFunction; + error?: FieldPolicy | FieldReadFunction; id?: FieldPolicy | FieldReadFunction; question?: FieldPolicy | FieldReadFunction; - sources?: FieldPolicy | FieldReadFunction; -}; -export type MessageAnswerToQuestionSourceKeySpecifier = ('title' | 'uri' | MessageAnswerToQuestionSourceKeySpecifier)[]; -export type MessageAnswerToQuestionSourceFieldPolicy = { - title?: FieldPolicy | FieldReadFunction; - uri?: FieldPolicy | FieldReadFunction; + success?: FieldPolicy | FieldReadFunction; }; export type MetadataKeySpecifier = ('services' | MetadataKeySpecifier)[]; export type MetadataFieldPolicy = { @@ -1961,6 +1954,7 @@ export type MutationKeySpecifier = ( | 'aiServerPersonaServiceIngest' | 'aiServerUpdateAiPersonaService' | 'applyForEntryRoleOnRoleSet' + | 'askChatGuidanceQuestion' | 'assignLicensePlanToAccount' | 'assignLicensePlanToSpace' | 'assignOrganizationRoleToUser' @@ -1984,6 +1978,7 @@ export type MutationKeySpecifier = ( | 'createActor' | 'createActorGroup' | 'createCalloutOnCollaboration' + | 'createChatGuidanceRoom' | 'createContributionOnCallout' | 'createDiscussion' | 'createEventOnCalendar' @@ -2126,6 +2121,7 @@ export type MutationFieldPolicy = { aiServerPersonaServiceIngest?: FieldPolicy | FieldReadFunction; aiServerUpdateAiPersonaService?: FieldPolicy | FieldReadFunction; applyForEntryRoleOnRoleSet?: FieldPolicy | FieldReadFunction; + askChatGuidanceQuestion?: FieldPolicy | FieldReadFunction; assignLicensePlanToAccount?: FieldPolicy | FieldReadFunction; assignLicensePlanToSpace?: FieldPolicy | FieldReadFunction; assignOrganizationRoleToUser?: FieldPolicy | FieldReadFunction; @@ -2149,6 +2145,7 @@ export type MutationFieldPolicy = { createActor?: FieldPolicy | FieldReadFunction; createActorGroup?: FieldPolicy | FieldReadFunction; createCalloutOnCollaboration?: FieldPolicy | FieldReadFunction; + createChatGuidanceRoom?: FieldPolicy | FieldReadFunction; createContributionOnCallout?: FieldPolicy | FieldReadFunction; createDiscussion?: FieldPolicy | FieldReadFunction; createEventOnCalendar?: FieldPolicy | FieldReadFunction; @@ -2638,8 +2635,6 @@ export type QueryKeySpecifier = ( | 'adminCommunicationMembership' | 'adminCommunicationOrphanedUsage' | 'aiServer' - | 'askChatGuidanceQuestion' - | 'askVirtualContributorQuestion' | 'exploreSpaces' | 'getSupportedVerifiedCredentialMetadata' | 'inputCreator' @@ -2677,8 +2672,6 @@ export type QueryFieldPolicy = { adminCommunicationMembership?: FieldPolicy | FieldReadFunction; adminCommunicationOrphanedUsage?: FieldPolicy | FieldReadFunction; aiServer?: FieldPolicy | FieldReadFunction; - askChatGuidanceQuestion?: FieldPolicy | FieldReadFunction; - askVirtualContributorQuestion?: FieldPolicy | FieldReadFunction; exploreSpaces?: FieldPolicy | FieldReadFunction; getSupportedVerifiedCredentialMetadata?: FieldPolicy | FieldReadFunction; inputCreator?: FieldPolicy | FieldReadFunction; @@ -3535,6 +3528,7 @@ export type UserKeySpecifier = ( | 'directRooms' | 'email' | 'firstName' + | 'guidanceRoom' | 'id' | 'isContactable' | 'lastName' @@ -3557,6 +3551,7 @@ export type UserFieldPolicy = { directRooms?: FieldPolicy | FieldReadFunction; email?: FieldPolicy | FieldReadFunction; firstName?: FieldPolicy | FieldReadFunction; + guidanceRoom?: FieldPolicy | FieldReadFunction; id?: FieldPolicy | FieldReadFunction; isContactable?: FieldPolicy | FieldReadFunction; lastName?: FieldPolicy | FieldReadFunction; @@ -4289,13 +4284,6 @@ export type StrictTypedTypePolicies = { keyFields?: false | MessageAnswerQuestionKeySpecifier | (() => undefined | MessageAnswerQuestionKeySpecifier); fields?: MessageAnswerQuestionFieldPolicy; }; - MessageAnswerToQuestionSource?: Omit & { - keyFields?: - | false - | MessageAnswerToQuestionSourceKeySpecifier - | (() => undefined | MessageAnswerToQuestionSourceKeySpecifier); - fields?: MessageAnswerToQuestionSourceFieldPolicy; - }; Metadata?: Omit & { keyFields?: false | MetadataKeySpecifier | (() => undefined | MetadataKeySpecifier); fields?: MetadataFieldPolicy; diff --git a/src/core/apollo/generated/apollo-hooks.ts b/src/core/apollo/generated/apollo-hooks.ts index f321342e1e..5750ecc64c 100644 --- a/src/core/apollo/generated/apollo-hooks.ts +++ b/src/core/apollo/generated/apollo-hooks.ts @@ -21883,70 +21883,221 @@ export type ResetChatGuidanceMutationOptions = Apollo.BaseMutationOptions< SchemaTypes.ResetChatGuidanceMutation, SchemaTypes.ResetChatGuidanceMutationVariables >; +export const CreateGuidanceRoomDocument = gql` + mutation createGuidanceRoom { + createChatGuidanceRoom { + id + } + } +`; +export type CreateGuidanceRoomMutationFn = Apollo.MutationFunction< + SchemaTypes.CreateGuidanceRoomMutation, + SchemaTypes.CreateGuidanceRoomMutationVariables +>; + +/** + * __useCreateGuidanceRoomMutation__ + * + * To run a mutation, you first call `useCreateGuidanceRoomMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateGuidanceRoomMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createGuidanceRoomMutation, { data, loading, error }] = useCreateGuidanceRoomMutation({ + * variables: { + * }, + * }); + */ +export function useCreateGuidanceRoomMutation( + baseOptions?: Apollo.MutationHookOptions< + SchemaTypes.CreateGuidanceRoomMutation, + SchemaTypes.CreateGuidanceRoomMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation( + CreateGuidanceRoomDocument, + options + ); +} + +export type CreateGuidanceRoomMutationHookResult = ReturnType; +export type CreateGuidanceRoomMutationResult = Apollo.MutationResult; +export type CreateGuidanceRoomMutationOptions = Apollo.BaseMutationOptions< + SchemaTypes.CreateGuidanceRoomMutation, + SchemaTypes.CreateGuidanceRoomMutationVariables +>; export const AskChatGuidanceQuestionDocument = gql` - query askChatGuidanceQuestion($chatData: ChatGuidanceInput!) { + mutation askChatGuidanceQuestion($chatData: ChatGuidanceInput!) { askChatGuidanceQuestion(chatData: $chatData) { id - answer - question - sources { - uri - title + success + } + } +`; +export type AskChatGuidanceQuestionMutationFn = Apollo.MutationFunction< + SchemaTypes.AskChatGuidanceQuestionMutation, + SchemaTypes.AskChatGuidanceQuestionMutationVariables +>; + +/** + * __useAskChatGuidanceQuestionMutation__ + * + * To run a mutation, you first call `useAskChatGuidanceQuestionMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useAskChatGuidanceQuestionMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [askChatGuidanceQuestionMutation, { data, loading, error }] = useAskChatGuidanceQuestionMutation({ + * variables: { + * chatData: // value for 'chatData' + * }, + * }); + */ +export function useAskChatGuidanceQuestionMutation( + baseOptions?: Apollo.MutationHookOptions< + SchemaTypes.AskChatGuidanceQuestionMutation, + SchemaTypes.AskChatGuidanceQuestionMutationVariables + > +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useMutation< + SchemaTypes.AskChatGuidanceQuestionMutation, + SchemaTypes.AskChatGuidanceQuestionMutationVariables + >(AskChatGuidanceQuestionDocument, options); +} + +export type AskChatGuidanceQuestionMutationHookResult = ReturnType; +export type AskChatGuidanceQuestionMutationResult = Apollo.MutationResult; +export type AskChatGuidanceQuestionMutationOptions = Apollo.BaseMutationOptions< + SchemaTypes.AskChatGuidanceQuestionMutation, + SchemaTypes.AskChatGuidanceQuestionMutationVariables +>; +export const GuidanceRoomIdDocument = gql` + query GuidanceRoomId { + me { + user { + id + guidanceRoom { + id + } } } } `; /** - * __useAskChatGuidanceQuestionQuery__ + * __useGuidanceRoomIdQuery__ * - * To run a query within a React component, call `useAskChatGuidanceQuestionQuery` and pass it any options that fit your needs. - * When your component renders, `useAskChatGuidanceQuestionQuery` returns an object from Apollo Client that contains loading, error, and data properties + * To run a query within a React component, call `useGuidanceRoomIdQuery` and pass it any options that fit your needs. + * When your component renders, `useGuidanceRoomIdQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example - * const { data, loading, error } = useAskChatGuidanceQuestionQuery({ + * const { data, loading, error } = useGuidanceRoomIdQuery({ * variables: { - * chatData: // value for 'chatData' * }, * }); */ -export function useAskChatGuidanceQuestionQuery( +export function useGuidanceRoomIdQuery( + baseOptions?: Apollo.QueryHookOptions +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useQuery( + GuidanceRoomIdDocument, + options + ); +} + +export function useGuidanceRoomIdLazyQuery( + baseOptions?: Apollo.LazyQueryHookOptions +) { + const options = { ...defaultOptions, ...baseOptions }; + return Apollo.useLazyQuery( + GuidanceRoomIdDocument, + options + ); +} + +export type GuidanceRoomIdQueryHookResult = ReturnType; +export type GuidanceRoomIdLazyQueryHookResult = ReturnType; +export type GuidanceRoomIdQueryResult = Apollo.QueryResult< + SchemaTypes.GuidanceRoomIdQuery, + SchemaTypes.GuidanceRoomIdQueryVariables +>; +export function refetchGuidanceRoomIdQuery(variables?: SchemaTypes.GuidanceRoomIdQueryVariables) { + return { query: GuidanceRoomIdDocument, variables: variables }; +} + +export const GuidanceRoomMessagesDocument = gql` + query GuidanceRoomMessages($roomId: UUID!) { + lookup { + room(ID: $roomId) { + ...CommentsWithMessages + } + } + } + ${CommentsWithMessagesFragmentDoc} +`; + +/** + * __useGuidanceRoomMessagesQuery__ + * + * To run a query within a React component, call `useGuidanceRoomMessagesQuery` and pass it any options that fit your needs. + * When your component renders, `useGuidanceRoomMessagesQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGuidanceRoomMessagesQuery({ + * variables: { + * roomId: // value for 'roomId' + * }, + * }); + */ +export function useGuidanceRoomMessagesQuery( baseOptions: Apollo.QueryHookOptions< - SchemaTypes.AskChatGuidanceQuestionQuery, - SchemaTypes.AskChatGuidanceQuestionQueryVariables + SchemaTypes.GuidanceRoomMessagesQuery, + SchemaTypes.GuidanceRoomMessagesQueryVariables > ) { const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useQuery( - AskChatGuidanceQuestionDocument, + return Apollo.useQuery( + GuidanceRoomMessagesDocument, options ); } -export function useAskChatGuidanceQuestionLazyQuery( +export function useGuidanceRoomMessagesLazyQuery( baseOptions?: Apollo.LazyQueryHookOptions< - SchemaTypes.AskChatGuidanceQuestionQuery, - SchemaTypes.AskChatGuidanceQuestionQueryVariables + SchemaTypes.GuidanceRoomMessagesQuery, + SchemaTypes.GuidanceRoomMessagesQueryVariables > ) { const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useLazyQuery< - SchemaTypes.AskChatGuidanceQuestionQuery, - SchemaTypes.AskChatGuidanceQuestionQueryVariables - >(AskChatGuidanceQuestionDocument, options); + return Apollo.useLazyQuery( + GuidanceRoomMessagesDocument, + options + ); } -export type AskChatGuidanceQuestionQueryHookResult = ReturnType; -export type AskChatGuidanceQuestionLazyQueryHookResult = ReturnType; -export type AskChatGuidanceQuestionQueryResult = Apollo.QueryResult< - SchemaTypes.AskChatGuidanceQuestionQuery, - SchemaTypes.AskChatGuidanceQuestionQueryVariables +export type GuidanceRoomMessagesQueryHookResult = ReturnType; +export type GuidanceRoomMessagesLazyQueryHookResult = ReturnType; +export type GuidanceRoomMessagesQueryResult = Apollo.QueryResult< + SchemaTypes.GuidanceRoomMessagesQuery, + SchemaTypes.GuidanceRoomMessagesQueryVariables >; -export function refetchAskChatGuidanceQuestionQuery(variables: SchemaTypes.AskChatGuidanceQuestionQueryVariables) { - return { query: AskChatGuidanceQuestionDocument, variables: variables }; +export function refetchGuidanceRoomMessagesQuery(variables: SchemaTypes.GuidanceRoomMessagesQueryVariables) { + return { query: GuidanceRoomMessagesDocument, variables: variables }; } export const JourneyRouteResolverDocument = gql` diff --git a/src/core/apollo/generated/graphql-schema.ts b/src/core/apollo/generated/graphql-schema.ts index 062cb72a98..824687471f 100644 --- a/src/core/apollo/generated/graphql-schema.ts +++ b/src/core/apollo/generated/graphql-schema.ts @@ -668,33 +668,12 @@ export type AiPersonaServiceIngestInput = { aiPersonaServiceID: Scalars['UUID']; }; -export type AiPersonaServiceQuestionInput = { - /** Virtual Persona Type. */ - aiPersonaServiceID: Scalars['UUID']; - /** The ID of the context, the Virtual Persona is asked a question. */ - contextID?: InputMaybe; - /** The Virtual Contributor description. */ - description?: InputMaybe; - /** The Virtual Contributor displayName. */ - displayName: Scalars['String']; - /** The Virtual Contributor interaciton part of which is this question. */ - interactionID?: InputMaybe; - /** The question that is being asked. */ - question: Scalars['String']; - /** The ID of the message thread where the Virtual Contributor is asked a question if applicable. */ - threadID?: InputMaybe; - /** User identifier used internaly by the engine. */ - userID?: InputMaybe; -}; - export type AiServer = { __typename?: 'AiServer'; /** A particular AiPersonaService */ aiPersonaService: AiPersonaService; /** The AiPersonaServices on this aiServer */ aiPersonaServices: Array; - /** Ask the virtual persona engine for guidance. */ - askAiPersonaServiceQuestion: MessageAnswerQuestion; /** The authorization rules for the entity */ authorization?: Maybe; /** The date at which the entity was created. */ @@ -711,10 +690,6 @@ export type AiServerAiPersonaServiceArgs = { ID: Scalars['UUID']; }; -export type AiServerAskAiPersonaServiceQuestionArgs = { - aiPersonaQuestionInput: AiPersonaServiceQuestionInput; -}; - export type Application = { __typename?: 'Application'; /** The authorization rules for the entity */ @@ -1236,7 +1211,7 @@ export enum CalloutVisibility { export type ChatGuidanceAnswerRelevanceInput = { /** The answer id. */ - id: Scalars['UUID']; + id: Scalars['String']; /** Is the answer relevant or not. */ relevant: Scalars['Boolean']; }; @@ -3232,23 +3207,14 @@ export type Message = { /** A detailed answer to a question, typically from an AI service. */ export type MessageAnswerQuestion = { __typename?: 'MessageAnswerQuestion'; - /** The answer to the question */ - answer: Scalars['String']; + /** Error message if an error occurred */ + error?: Maybe; /** The id of the answer; null if an error was returned */ id?: Maybe; /** The original question */ question: Scalars['String']; - /** The sources used to answer the question */ - sources?: Maybe>; -}; - -/** A source used in a detailed answer to a question. */ -export type MessageAnswerToQuestionSource = { - __typename?: 'MessageAnswerToQuestionSource'; - /** The title of the source */ - title?: Maybe; - /** The URI of the source */ - uri?: Maybe; + /** Message successfully sent. If false, error will have the reason. */ + success: Scalars['Boolean']; }; export type Metadata = { @@ -3317,6 +3283,8 @@ export type Mutation = { aiServerUpdateAiPersonaService: AiPersonaService; /** Apply to join the specified RoleSet in the entry Role. */ applyForEntryRoleOnRoleSet: Application; + /** Ask the chat engine for guidance. */ + askChatGuidanceQuestion: MessageAnswerQuestion; /** Assign the specified LicensePlan to an Account. */ assignLicensePlanToAccount: Account; /** Assign the specified LicensePlan to a Space. */ @@ -3363,6 +3331,8 @@ export type Mutation = { createActorGroup: ActorGroup; /** Create a new Callout on the Collaboration. */ createCalloutOnCollaboration: Callout; + /** Create a guidance chat room. */ + createChatGuidanceRoom?: Maybe; /** Create a new Contribution on the Callout. */ createContributionOnCallout: CalloutContribution; /** Creates a new Discussion as part of this Forum. */ @@ -3661,6 +3631,10 @@ export type MutationApplyForEntryRoleOnRoleSetArgs = { applicationData: ApplyForEntryRoleOnRoleSetInput; }; +export type MutationAskChatGuidanceQuestionArgs = { + chatData: ChatGuidanceInput; +}; + export type MutationAssignLicensePlanToAccountArgs = { planData: AssignLicensePlanToAccount; }; @@ -4736,10 +4710,6 @@ export type Query = { adminCommunicationOrphanedUsage: CommunicationAdminOrphanedUsageResult; /** Alkemio AiServer */ aiServer: AiServer; - /** Ask the chat engine for guidance. */ - askChatGuidanceQuestion: MessageAnswerQuestion; - /** Ask the virtual contributor a question directly. */ - askVirtualContributorQuestion: MessageAnswerQuestion; /** Active Spaces only, order by most active in the past X days. */ exploreSpaces: Array; /** Get supported credential metadata */ @@ -4818,14 +4788,6 @@ export type QueryAdminCommunicationMembershipArgs = { communicationData: CommunicationAdminMembershipInput; }; -export type QueryAskChatGuidanceQuestionArgs = { - chatData: ChatGuidanceInput; -}; - -export type QueryAskVirtualContributorQuestionArgs = { - virtualContributorQuestionInput: VirtualContributorQuestionInput; -}; - export type QueryExploreSpacesArgs = { options?: InputMaybe; }; @@ -6634,6 +6596,8 @@ export type User = Contributor & { /** The email address for this User. */ email: Scalars['String']; firstName: Scalars['String']; + /** Guidance Chat Room for this user */ + guidanceRoom?: Maybe; /** The ID of the Contributor */ id: Scalars['UUID']; /** Can a message be sent to this User. */ @@ -6812,21 +6776,6 @@ export type VirtualContributor = Contributor & { updatedDate?: Maybe; }; -export type VirtualContributorQuestionInput = { - /** The space in which context the Virtual Contributor is asked a question */ - contextSpaceID?: InputMaybe; - /** The question that is being asked. */ - question: Scalars['String']; - /** The ID of the message thread where the Virtual Contributor is asked a question */ - threadID?: InputMaybe; - /** User identifier used internaly by the engine */ - userID?: InputMaybe; - /** The Virtual Contributor interaciton part of which is this question */ - vcInteractionID?: InputMaybe; - /** Virtual Contributor to be asked. */ - virtualContributorID: Scalars['UUID']; -}; - export enum VirtualContributorStatus { Initializing = 'INITIALIZING', Ready = 'READY', @@ -27552,19 +27501,161 @@ export type ResetChatGuidanceMutationVariables = Exact<{ [key: string]: never }> export type ResetChatGuidanceMutation = { __typename?: 'Mutation'; resetChatGuidance: boolean }; -export type AskChatGuidanceQuestionQueryVariables = Exact<{ +export type CreateGuidanceRoomMutationVariables = Exact<{ [key: string]: never }>; + +export type CreateGuidanceRoomMutation = { + __typename?: 'Mutation'; + createChatGuidanceRoom?: { __typename?: 'Room'; id: string } | undefined; +}; + +export type AskChatGuidanceQuestionMutationVariables = Exact<{ chatData: ChatGuidanceInput; }>; -export type AskChatGuidanceQuestionQuery = { +export type AskChatGuidanceQuestionMutation = { + __typename?: 'Mutation'; + askChatGuidanceQuestion: { __typename?: 'MessageAnswerQuestion'; id?: string | undefined; success: boolean }; +}; + +export type GuidanceRoomIdQueryVariables = Exact<{ [key: string]: never }>; + +export type GuidanceRoomIdQuery = { __typename?: 'Query'; - askChatGuidanceQuestion: { - __typename?: 'MessageAnswerQuestion'; - id?: string | undefined; - answer: string; - question: string; - sources?: - | Array<{ __typename?: 'MessageAnswerToQuestionSource'; uri?: string | undefined; title?: string | undefined }> + me: { + __typename?: 'MeQueryResults'; + user?: + | { __typename?: 'User'; id: string; guidanceRoom?: { __typename?: 'Room'; id: string } | undefined } + | undefined; + }; +}; + +export type GuidanceRoomMessagesQueryVariables = Exact<{ + roomId: Scalars['UUID']; +}>; + +export type GuidanceRoomMessagesQuery = { + __typename?: 'Query'; + lookup: { + __typename?: 'LookupQueryResults'; + room?: + | { + __typename?: 'Room'; + id: string; + messagesCount: number; + authorization?: + | { + __typename?: 'Authorization'; + id: string; + myPrivileges?: Array | undefined; + anonymousReadAccess: boolean; + } + | undefined; + messages: Array<{ + __typename?: 'Message'; + id: string; + message: string; + timestamp: number; + threadID?: string | undefined; + reactions: Array<{ + __typename?: 'Reaction'; + id: string; + emoji: string; + sender?: + | { + __typename?: 'User'; + id: string; + profile: { __typename?: 'Profile'; id: string; displayName: string }; + } + | undefined; + }>; + sender?: + | { + __typename?: 'Organization'; + id: string; + nameID: string; + profile: { + __typename?: 'Profile'; + id: string; + displayName: string; + url: string; + description?: string | undefined; + avatar?: { __typename?: 'Visual'; id: string; uri: string; name: string } | undefined; + tagsets?: + | Array<{ + __typename?: 'Tagset'; + id: string; + name: string; + tags: Array; + allowedValues: Array; + type: TagsetType; + }> + | undefined; + location?: + | { __typename?: 'Location'; id: string; country?: string | undefined; city?: string | undefined } + | undefined; + }; + } + | { + __typename?: 'User'; + id: string; + nameID: string; + profile: { + __typename?: 'Profile'; + id: string; + displayName: string; + url: string; + description?: string | undefined; + avatar?: { __typename?: 'Visual'; id: string; uri: string; name: string } | undefined; + tagsets?: + | Array<{ + __typename?: 'Tagset'; + id: string; + name: string; + tags: Array; + allowedValues: Array; + type: TagsetType; + }> + | undefined; + location?: + | { __typename?: 'Location'; id: string; country?: string | undefined; city?: string | undefined } + | undefined; + }; + } + | { + __typename?: 'VirtualContributor'; + id: string; + nameID: string; + profile: { + __typename?: 'Profile'; + id: string; + displayName: string; + url: string; + description?: string | undefined; + avatar?: { __typename?: 'Visual'; id: string; uri: string; name: string } | undefined; + tagsets?: + | Array<{ + __typename?: 'Tagset'; + id: string; + name: string; + tags: Array; + allowedValues: Array; + type: TagsetType; + }> + | undefined; + location?: + | { __typename?: 'Location'; id: string; country?: string | undefined; city?: string | undefined } + | undefined; + }; + } + | undefined; + }>; + vcInteractions: Array<{ + __typename?: 'VcInteraction'; + id: string; + threadID: string; + virtualContributorID: string; + }>; + } | undefined; }; }; diff --git a/src/domain/collaboration/post/containers/PostDashboardContainer/PostDashboardContainer.tsx b/src/domain/collaboration/post/containers/PostDashboardContainer/PostDashboardContainer.tsx index 94103ee55d..1ed01a5e56 100644 --- a/src/domain/collaboration/post/containers/PostDashboardContainer/PostDashboardContainer.tsx +++ b/src/domain/collaboration/post/containers/PostDashboardContainer/PostDashboardContainer.tsx @@ -70,7 +70,7 @@ const PostDashboardContainer = ({ calloutId, postNameId, ...rendered }: PostDash () => _messages?.map(x => ({ id: x.id, - body: x.message, + message: x.message, author: x?.sender ? buildAuthorFromUser(x.sender) : undefined, createdAt: new Date(x.timestamp), reactions: x.reactions, diff --git a/src/domain/communication/discussion/pages/DiscussionPage.tsx b/src/domain/communication/discussion/pages/DiscussionPage.tsx index c0fe90ee4f..e8fec8faf9 100644 --- a/src/domain/communication/discussion/pages/DiscussionPage.tsx +++ b/src/domain/communication/discussion/pages/DiscussionPage.tsx @@ -67,7 +67,7 @@ export const DiscussionPage = ({ discussionNameId }: { discussionNameId: string messages: rawDiscussion.comments.messages?.map(m => ({ id: m.id, - body: m.message, + message: m.message, author: m.sender ? authors.getAuthor(m.sender?.id) : undefined, createdAt: new Date(m.timestamp), threadID: m.threadID, diff --git a/src/domain/communication/discussion/views/DiscussionView.tsx b/src/domain/communication/discussion/views/DiscussionView.tsx index 373d61e67e..ca74a010fc 100644 --- a/src/domain/communication/discussion/views/DiscussionView.tsx +++ b/src/domain/communication/discussion/views/DiscussionView.tsx @@ -51,7 +51,7 @@ export const DiscussionView = ({ id, author, createdAt: createdAt!, - body: description!, + message: description!, reactions: [], }), [id, author, createdAt, description] diff --git a/src/domain/communication/room/Comments/MessageView.tsx b/src/domain/communication/room/Comments/MessageView.tsx index 978d9c09da..fd0017eae8 100644 --- a/src/domain/communication/room/Comments/MessageView.tsx +++ b/src/domain/communication/room/Comments/MessageView.tsx @@ -104,7 +104,7 @@ export const MessageView = ({ {t('messaging.messageDeleted')} ) : ( - {message.body} + {message.message} )} {!message.deleted && ( diff --git a/src/domain/communication/room/Comments/useMessages.ts b/src/domain/communication/room/Comments/useMessages.ts index c604eb5123..2db37fffd3 100644 --- a/src/domain/communication/room/Comments/useMessages.ts +++ b/src/domain/communication/room/Comments/useMessages.ts @@ -36,7 +36,7 @@ export const useMessages = (messages: FetchedMessage[] | undefined) => return messages?.map(message => ({ id: message.id, threadID: message.threadID, - body: message.message, + message: message.message, author: message?.sender?.id ? buildAuthorFromUser(message.sender) : undefined, createdAt: new Date(message.timestamp), reactions: message.reactions, diff --git a/src/domain/communication/room/Comments/useRestoredMessages.ts b/src/domain/communication/room/Comments/useRestoredMessages.ts index 2779aa4fc4..34e742fb67 100644 --- a/src/domain/communication/room/Comments/useRestoredMessages.ts +++ b/src/domain/communication/room/Comments/useRestoredMessages.ts @@ -2,7 +2,7 @@ import { Message } from '../models/Message'; import { useMemo } from 'react'; import { keyBy, sortBy } from 'lodash'; -interface DeletedMessage extends Omit { +interface DeletedMessage extends Omit { deleted: true; } diff --git a/src/domain/communication/room/models/Message.ts b/src/domain/communication/room/models/Message.ts index dedaf22286..d859f9410e 100644 --- a/src/domain/communication/room/models/Message.ts +++ b/src/domain/communication/room/models/Message.ts @@ -5,7 +5,7 @@ export interface Message { threadID?: string; author?: Author; createdAt: Date; - body: string; + message: string; reactions: { id: string; emoji: string; diff --git a/src/domain/timeline/calendar/CalendarEventDetailContainer.tsx b/src/domain/timeline/calendar/CalendarEventDetailContainer.tsx index 343c4f2293..ebcdb3a2fe 100644 --- a/src/domain/timeline/calendar/CalendarEventDetailContainer.tsx +++ b/src/domain/timeline/calendar/CalendarEventDetailContainer.tsx @@ -68,7 +68,7 @@ const CalendarEventDetailContainer = ({ eventId, ...rendered }: CalendarEventDet () => _messages?.map(x => ({ id: x.id, - body: x.message, + message: x.message, author: x?.sender ? buildAuthorFromUser(x.sender) : undefined, createdAt: new Date(x.timestamp), reactions: x.reactions, diff --git a/src/main/guidance/chatWidget/ChatWidget.tsx b/src/main/guidance/chatWidget/ChatWidget.tsx index 2d8e6466ae..651a894491 100644 --- a/src/main/guidance/chatWidget/ChatWidget.tsx +++ b/src/main/guidance/chatWidget/ChatWidget.tsx @@ -1,17 +1,31 @@ import { cloneElement, ReactElement, useEffect, useLayoutEffect, useRef, useState } from 'react'; -import { Box, IconButton, IconButtonProps, Paper, SvgIconProps, Theme, Tooltip } from '@mui/material'; +import { + Box, + IconButton, + IconButtonProps, + Paper, + Skeleton, + SvgIconProps, + Theme, + Tooltip, + useTheme, +} from '@mui/material'; import ThumbUpOffAltIcon from '@mui/icons-material/ThumbUpOffAlt'; import ThumbDownOffAltIcon from '@mui/icons-material/ThumbDownOffAlt'; -import { addResponseMessage, dropMessages, renderCustomComponent, toggleWidget, Widget } from 'react-chat-widget'; +import { useUpdateAnswerRelevanceMutation } from '@/core/apollo/generated/apollo-hooks'; import { - useAskChatGuidanceQuestionQuery, - useResetChatGuidanceMutation, - useUpdateAnswerRelevanceMutation, -} from '@/core/apollo/generated/apollo-hooks'; + addResponseMessage, + addUserMessage, + dropMessages, + markAllAsRead, + renderCustomComponent, + setBadgeCount, + toggleWidget, + Widget, +} from 'react-chat-widget'; import logoSrc from '@/main/ui/logo/logoSmall.svg'; import { useTranslation } from 'react-i18next'; import 'react-chat-widget/lib/styles.css'; -import formatChatGuidanceResponseAsMarkdown from './formatChatGuidanceResponseAsMarkdown'; import ChatWidgetStyles from './ChatWidgetStyles'; import ChatWidgetTitle from './ChatWidgetTitle'; import ChatWidgetHelpDialog from './ChatWidgetHelpDialog'; @@ -23,6 +37,8 @@ import { Caption } from '@/core/ui/typography'; import { InfoOutlined } from '@mui/icons-material'; import { PLATFORM_NAVIGATION_MENU_Z_INDEX } from '@/main/ui/platformNavigation/constants'; import ChatWidgetMenu from './ChatWidgetMenu'; +import useChatGuidanceCommunication from './useChatGuidanceCommunication'; +import { useUserContext } from '../../../domain/community/user'; type FeedbackType = 'positive' | 'negative'; @@ -143,32 +159,26 @@ const Feedback = ({ answerId }: FeedbackProps) => { ); }; -export const useInitialChatWidgetMessage = () => { - const { t } = useTranslation(); - - useEffect(() => { - addResponseMessage(t('chatbot.intro')); - }, []); +const Loading = () => { + const theme = useTheme(); + return ( + + + + + ); }; const ChatWidget = () => { - const [newMessage, setNewMessage] = useState(null); - const { t, i18n } = useTranslation(); - const { data, loading } = useAskChatGuidanceQuestionQuery({ - variables: { chatData: { question: newMessage!, language: i18n.language.toUpperCase() } }, - skip: !newMessage, - fetchPolicy: 'network-only', - }); - - useEffect(() => { - if (data && !loading) { - const responseMessageMarkdown = formatChatGuidanceResponseAsMarkdown(data.askChatGuidanceQuestion, t); - addResponseMessage(responseMessageMarkdown, data.askChatGuidanceQuestion.id!); - renderCustomComponent(Feedback, { answerId: data.askChatGuidanceQuestion.id }); - } - }, [data, loading]); - + const { t } = useTranslation(); const [isHelpDialogOpen, setIsHelpDialogOpen] = useState(false); + const { messages, sendMessage, clearChat, loading } = useChatGuidanceCommunication(); + const { user } = useUserContext(); + const userId = user?.user.id; + + const handleNewUserMessage = async (newMessage: string) => { + await sendMessage(newMessage); + }; const [chatToggleTime, setChatToggleTime] = useState(Date.now()); @@ -212,23 +222,54 @@ const ChatWidget = () => { useLayoutEffect(setupMenuButton, [chatToggleTime]); - const [resetChatGuidance] = useResetChatGuidanceMutation(); + const handleClearChat = async () => { + await clearChat(); + setChatToggleTime(Date.now()); // Force a re-render + }; - const handleClearChat = () => { - resetChatGuidance(); + useEffect(() => { dropMessages(); - addResponseMessage(t('chatbot.intro')); - }; + if (messages && messages.length > 0) { + messages?.forEach(message => { + if (message.author?.id === userId) { + addUserMessage(message.message); + } else { + addResponseMessage(message.message); + } + }); + const lastMessage = messages[messages.length - 1]; + if (lastMessage.author?.id && lastMessage.author.id !== userId) { + // If the last message has an author and is not myself print the feedback buttons + renderCustomComponent(Feedback, { answerId: lastMessage.id }); + // And if the message is new, mark it as unread + if (lastMessage.createdAt > new Date(chatToggleTime)) { + setBadgeCount(1); + } else { + markAllAsRead(); + } + } else if (messages.length === 1) { + // Always mark as unread the intro message + setBadgeCount(1); + } else { + markAllAsRead(); + } + } + if (loading) { + renderCustomComponent(Loading, undefined); + } + }, [messages, loading]); return ( <> setIsHelpDialogOpen(true)} />} - subtitle={<>} - handleNewUserMessage={setNewMessage} - handleToggle={() => setChatToggleTime(Date.now())} + title={ setIsHelpDialogOpen(true)} />} + subtitle={null} + handleNewUserMessage={handleNewUserMessage} + handleToggle={() => { + setChatToggleTime(Date.now()); + }} /> setIsHelpDialogOpen(false)} /> diff --git a/src/main/guidance/chatWidget/ChatWidgetMutations.graphql b/src/main/guidance/chatWidget/ChatWidgetMutations.graphql index ea1f7d7da1..f5af0f76c0 100644 --- a/src/main/guidance/chatWidget/ChatWidgetMutations.graphql +++ b/src/main/guidance/chatWidget/ChatWidgetMutations.graphql @@ -4,4 +4,18 @@ mutation updateAnswerRelevance($input: ChatGuidanceAnswerRelevanceInput!) { mutation resetChatGuidance { resetChatGuidance -} \ No newline at end of file +} + +mutation createGuidanceRoom { + createChatGuidanceRoom { + id + } +} + +mutation askChatGuidanceQuestion($chatData: ChatGuidanceInput!) { + askChatGuidanceQuestion(chatData: $chatData) { + id + success + } +} + diff --git a/src/main/guidance/chatWidget/ChatWidgetQueries.graphql b/src/main/guidance/chatWidget/ChatWidgetQueries.graphql index 560ecd14fd..4a0f9bbb03 100644 --- a/src/main/guidance/chatWidget/ChatWidgetQueries.graphql +++ b/src/main/guidance/chatWidget/ChatWidgetQueries.graphql @@ -1,11 +1,18 @@ -query askChatGuidanceQuestion($chatData: ChatGuidanceInput!) { - askChatGuidanceQuestion(chatData: $chatData) { - id - answer - question - sources { - uri - title +query GuidanceRoomId { + me { + user { + id + guidanceRoom { + id + } + } + } +} + +query GuidanceRoomMessages($roomId: UUID!) { + lookup { + room (ID: $roomId) { + ...CommentsWithMessages } } } diff --git a/src/main/guidance/chatWidget/ChatWidgetStyles.tsx b/src/main/guidance/chatWidget/ChatWidgetStyles.tsx index 5a16b883e2..b40fb9508c 100644 --- a/src/main/guidance/chatWidget/ChatWidgetStyles.tsx +++ b/src/main/guidance/chatWidget/ChatWidgetStyles.tsx @@ -1,9 +1,10 @@ import 'react-chat-widget/lib/styles.css'; import { Box, BoxProps } from '@mui/material'; import { gutters } from '@/core/ui/grid/utils'; -import { SOURCES_HEADING_TAG_HTML } from './formatChatGuidanceResponseAsMarkdown'; import { forwardRef } from 'react'; +export const SOURCES_HEADING_TAG_HTML = 'h5'; // In the server there's a '#####' markdown tag + const ChatWidgetStyles = forwardRef((props, ref) => ( { - const { answer, sources } = response; - const sourcesMarkdown = - !sources || sources.length === 0 - ? '' - : ` - -${SOURCES_HEADING_TAG_MARKDOWN} ${t('common.sources')}: - -${sources - .map(source => { - const title = source.title ?? source.uri; - const uri = source.uri ?? ''; - - return `- [${title}](${uri})`; - }) - .join('\n')} - `; - - return `${answer}${sourcesMarkdown}`; -}; - -export default formatChatGuidanceResponseAsMarkdown; diff --git a/src/main/guidance/chatWidget/useChatGuidanceCommunication.ts b/src/main/guidance/chatWidget/useChatGuidanceCommunication.ts new file mode 100644 index 0000000000..f402a5847e --- /dev/null +++ b/src/main/guidance/chatWidget/useChatGuidanceCommunication.ts @@ -0,0 +1,111 @@ +import { useMemo, useState } from 'react'; +import { + AskChatGuidanceQuestionMutationOptions, + useAskChatGuidanceQuestionMutation, + useCreateGuidanceRoomMutation, + useGuidanceRoomIdQuery, + useGuidanceRoomMessagesQuery, + useResetChatGuidanceMutation, +} from '../../../core/apollo/generated/apollo-hooks'; +import useSubscribeOnRoomEvents from '../../../domain/collaboration/callout/useSubscribeOnRoomEvents'; +import { Message } from '../../../domain/communication/room/models/Message'; +import { buildAuthorFromUser } from '../../../domain/community/user/utils/buildAuthorFromUser'; +import { useTranslation } from 'react-i18next'; +import useLoadingState from '../../../domain/shared/utils/useLoadingState'; + +interface Provided { + loading?: boolean; + messages?: Message[]; + clearChat: () => Promise; + sendMessage: (message: string) => Promise; + isSubscribedToMessages: boolean; +} + +const useChatGuidanceCommunication = (): Provided => { + const { t, i18n } = useTranslation(); + + const [createGuidanceRoom] = useCreateGuidanceRoomMutation(); + const [resetChatGuidance] = useResetChatGuidanceMutation(); + + const [sendingFirstMessage, setSendingFirstMessage] = useState(false); + + const { data: roomIdData, loading: roomIdLoading, refetch: refetchGuidanceRoomId } = useGuidanceRoomIdQuery(); + const roomId = roomIdData?.me.user?.guidanceRoom?.id; + + const { data: messagesData, loading: messagesLoading } = useGuidanceRoomMessagesQuery({ + variables: { + roomId: roomId!, + }, + skip: !roomId || sendingFirstMessage, + }); + + const messages: Message[] = useMemo(() => { + const introMessage = { + id: '__intro', + createdAt: new Date(), + reactions: [], + message: t('chatbot.intro'), + author: undefined, + }; + + if (messagesData?.lookup.room?.messages.length) { + return [ + introMessage, + ...messagesData?.lookup.room?.messages?.map(message => ({ + id: message.id, + threadID: message.threadID, + message: message.message, + author: message?.sender?.id ? buildAuthorFromUser(message.sender) : undefined, + createdAt: new Date(message.timestamp), + reactions: message.reactions, + })), + ]; + } else { + // No messages or just no room at all => Return just one message with the intro text + return [introMessage]; + } + }, [messagesData?.lookup.room?.messages, roomId, sendingFirstMessage, roomIdLoading, messagesLoading]); + + const isSubscribedToMessages = useSubscribeOnRoomEvents(roomId, !roomId); + + const [askChatGuidanceQuestion] = useAskChatGuidanceQuestionMutation(); + const askQuestion = async ( + question: string, + refetchQueries?: AskChatGuidanceQuestionMutationOptions['refetchQueries'] + ) => + askChatGuidanceQuestion({ + variables: { + chatData: { question, language: i18n.language.toUpperCase() }, + }, + refetchQueries, + awaitRefetchQueries: true, + }); + + const handleSendMessage = async (message: string): Promise => { + if (!roomId) { + setSendingFirstMessage(true); + await createGuidanceRoom({ + refetchQueries: ['GuidanceRoomId', 'GuidanceRoomMessages'], + }); + await askQuestion(message); + setSendingFirstMessage(false); + } else { + await askQuestion(message, ['GuidanceRoomMessages']); + } + }; + + const [clearChat, loadingClearChat] = useLoadingState(async () => { + await resetChatGuidance(); + await refetchGuidanceRoomId(); + }); + + return { + loading: roomIdLoading || messagesLoading || loadingClearChat, + messages, + isSubscribedToMessages, + clearChat, + sendMessage: handleSendMessage, + }; +}; + +export default useChatGuidanceCommunication; diff --git a/src/root.tsx b/src/root.tsx index d54ee9b8d6..8e6961099c 100644 --- a/src/root.tsx +++ b/src/root.tsx @@ -19,7 +19,6 @@ import { fontFamilySourceSans, subHeading } from '@/core/ui/typography/themeTypo import { ApmProvider, ApmUserSetter } from '@/core/analytics/apm/context'; import { UserGeoProvider } from '@/core/analytics/geo'; import { SentryTransactionScopeContextProvider } from '@/core/analytics/SentryTransactionScopeContext'; -import { useInitialChatWidgetMessage } from '@/main/guidance/chatWidget/ChatWidget'; import { PendingMembershipsDialogProvider } from '@/domain/community/pendingMembership/PendingMembershipsDialogContext'; import { NotFoundErrorBoundary } from '@/core/notFound/NotFoundErrorBoundary'; import { Error404 } from '@/core/pages/Errors/Error404'; @@ -67,8 +66,6 @@ const GlobalStyles: FC = ({ children }) => { }; const Root: FC = () => { - useInitialChatWidgetMessage(); - return (