From 8a64316f4dc830a05a869216ac6a5129b36c316a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Mendes?= Date: Sun, 5 Mar 2023 16:42:45 +0000 Subject: [PATCH] commit changes on CHatGPT-APP - add support to Chat and Channels --- .../src/adaptiveCards/chatGPTAnswerCard.ts | 17 +++- .../src/components/ChatGpt/ChatGptControl.tsx | 13 ++- .../components/RenderAnswer/RenderAnswer.tsx | 17 ++-- .../RenderMessages/RenderMessages.tsx | 58 +++++++++++- .../SendMessageToChat/SendMessageToChat.tsx | 6 +- .../src/hooks/useAdaptiveCardsUtils.ts | 2 +- .../src/hooks/useGraphAPI.ts | 63 +++++++++---- .../src/hooks/useHtmlUtils.ts | 13 +++ .../src/hooks/useSendMessageToTeams.ts | 94 ++++++++----------- .../src/models/IAdaptivecardData.ts | 1 + .../src/models/IChatGptProps.ts | 3 + .../src/models/IGlobalState.ts | 3 + .../src/models/IRenderAnswerProps.ts | 1 + .../src/webparts/chatGpt/ChatGptWebPart.ts | 4 + 14 files changed, 199 insertions(+), 96 deletions(-) create mode 100644 samples/react-chatgpt-app/src/hooks/useHtmlUtils.ts diff --git a/samples/react-chatgpt-app/src/adaptiveCards/chatGPTAnswerCard.ts b/samples/react-chatgpt-app/src/adaptiveCards/chatGPTAnswerCard.ts index 56bd5d458..3893eefce 100644 --- a/samples/react-chatgpt-app/src/adaptiveCards/chatGPTAnswerCard.ts +++ b/samples/react-chatgpt-app/src/adaptiveCards/chatGPTAnswerCard.ts @@ -1,7 +1,7 @@ export const CARD = { "type": "AdaptiveCard", "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "version": "1.2", + "version": "1.3", "body": [ { "type": "ColumnSet", @@ -56,6 +56,21 @@ export const CARD = { } ] }, + { + "type": "Container", + "spacing": "medium", + "style": "emphasis", + "items": [ + { + "type": "TextBlock", + "text": "${$root.question}", + "wrap": true, + "horizontalAlignment": "Right", + "color": "Accent" + } + ], + "$when": "${length($root.question)>0}" + }, { "type": "Container", "spacing": "Padding", diff --git a/samples/react-chatgpt-app/src/components/ChatGpt/ChatGptControl.tsx b/samples/react-chatgpt-app/src/components/ChatGpt/ChatGptControl.tsx index 7bfa078be..5d71660fe 100644 --- a/samples/react-chatgpt-app/src/components/ChatGpt/ChatGptControl.tsx +++ b/samples/react-chatgpt-app/src/components/ChatGpt/ChatGptControl.tsx @@ -26,14 +26,19 @@ export const ChatGptControl: React.FunctionComponent = ( const { context } = props; const [appGlobalState, setAppGlobalState] = useAtom(globalState); const { containerStyles } = useChatGptStyles(); - const { hasTeamsContext, chatId } = appGlobalState; + const { hasTeamsContext, chatId, channelId } = appGlobalState; const { getTenantProperty } = useSpAPI(context); const [isLoading, setIsLoading] = React.useState(true); const [error, setError] = React.useState(undefined); const isInChat = React.useMemo((): boolean => { - return hasTeamsContext && !!chatId; - }, [chatId, hasTeamsContext]); + if (hasTeamsContext && (chatId )) { + return true; + } + return false; + }, [chatId, channelId, hasTeamsContext]); + +const isInChannel = React.useMemo(() => !!channelId, [ channelId]); const isPreviewChatId = React.useMemo((): boolean => { if (isInChat) { @@ -91,7 +96,7 @@ export const ChatGptControl: React.FunctionComponent = ( return ( <> -
+
diff --git a/samples/react-chatgpt-app/src/components/RenderAnswer/RenderAnswer.tsx b/samples/react-chatgpt-app/src/components/RenderAnswer/RenderAnswer.tsx index 1ecf7b79d..2bbcd4eb8 100644 --- a/samples/react-chatgpt-app/src/components/RenderAnswer/RenderAnswer.tsx +++ b/samples/react-chatgpt-app/src/components/RenderAnswer/RenderAnswer.tsx @@ -14,7 +14,6 @@ import { showNotification } from '@mantine/notifications'; import { CARD } from '../../adaptiveCards/chatGPTAnswerCard'; import { globalState } from '../../atoms'; import { useAdaptiveCardsUtils } from '../../hooks/useAdaptiveCardsUtils'; -import { useGraphAPI } from '../../hooks/useGraphAPI'; import { useSendMessageToTeams } from '../../hooks/useSendMessageToTeams'; import { IAdaptativeCardData } from '../../models/IAdaptivecardData'; import { IRenderAnswerProps } from '../../models/IRenderAnswerProps'; @@ -26,24 +25,25 @@ import { SendMessageToChat } from '../SendMessageToChat/SendMessageToChat'; export const RenderAnswer: React.FunctionComponent = ( props: React.PropsWithChildren ) => { - const { answer } = props; + const { answer, question } = props; const { answerStyles, nameStyles, answerContainerStyles, controlStyles } = useChatGptStyles(); const [appGlobalState] = useAtom(globalState); - const { lastConversation, context, chatId } = appGlobalState; + const { lastConversation, context, chatId, teamsId, channelId, parentMessageId, hasTeamsContext } = appGlobalState; const [error, setError] = React.useState(undefined); - const { sendMessage } = useGraphAPI(context); + const { createAdaptiveCard } = useAdaptiveCardsUtils(); const { sendAdativeCardToUsers } = useSendMessageToTeams(context); const hasError = React.useMemo(() => error !== undefined, [error]); + const onSendMessageToChat = React.useCallback(async () => { - if (answer && chatId) { + if (answer && hasTeamsContext ) { try { - const cardData: IAdaptativeCardData = { date: format(new Date(), "PPpp"), answer: answer }; + const cardData: IAdaptativeCardData = { date: format(new Date(), "PPpp"), answer: answer, question: question ?? ""}; const card = createAdaptiveCard(cardData, CARD); console.log("carddata", cardData); console.log("card", card); - await sendAdativeCardToUsers(card, cardData, chatId); + await sendAdativeCardToUsers(card, cardData, chatId, teamsId, channelId, parentMessageId); showNotification({ title: strings.ChatGPTAppNotificationTitle, @@ -57,7 +57,7 @@ export const RenderAnswer: React.FunctionComponent = ( setError(error); } } - }, [answer, chatId, sendMessage, sendAdativeCardToUsers, createAdaptiveCard]); + }, [answer,teamsId,channelId, hasTeamsContext, chatId, sendAdativeCardToUsers, createAdaptiveCard, question, parentMessageId]); const islastConversation = React.useMemo(() => lastConversation === "answer", [lastConversation]); return ( @@ -86,7 +86,6 @@ export const RenderAnswer: React.FunctionComponent = ( -
= ( ) => { const { isShowMessages } = props; const [appGlobalState] = useAtom(globalState); - const { context, appId, AzureFunctionUrl } = appGlobalState; + const { context, appId, AzureFunctionUrl, parentMessageId, chatId, teamsId, channelId } = appGlobalState; const { textFieldStyles, controlStyles, buttonIconStyles } = useChatGptStyles(); const [conversation, setConversation] = React.useState([]); const [textToAsk, setTextToAsk] = React.useState(""); @@ -31,6 +37,12 @@ export const RenderMessages: React.FunctionComponent = ( const { getCompletion } = useChatGpt(context, appId, AzureFunctionUrl); const scrollRef = React.useRef(null); const [error, setError] = React.useState(undefined); + const { getChatParentMessage, getChannelParentMessage } = useGraphAPI(context); + const executeAutoGetComplete = React.useRef(false); + const { getTextFromHtml } = useHtmlUtils(); + + const hasParentMessage = React.useMemo(() => !!parentMessageId , [parentMessageId]); + const isInChannel = React.useMemo(() => !!teamsId && !!channelId, [teamsId, channelId]); const hasError = React.useMemo(() => error !== undefined, [error]); @@ -54,8 +66,8 @@ export const RenderMessages: React.FunctionComponent = ( ); const addAnswer = React.useCallback( - (answer: string) => { - const newAnswer = ; + (answer: string, question?: string) => { + const newAnswer = ; setConversation((prev) => { return [...prev, newAnswer]; }); @@ -92,6 +104,44 @@ export const RenderMessages: React.FunctionComponent = ( [onSubmit] ); + const runAutoGetComplete = React.useCallback(async () => { + try { + let messageDetails: ChatMessage = undefined; + if (!isInChannel ) { + messageDetails = await getChatParentMessage(chatId, parentMessageId); + } else { + messageDetails = await getChannelParentMessage(teamsId, channelId, parentMessageId); + } + const { body } = messageDetails; + if (body) { + setError(undefined); + const { content, } = body; + console.log(body); + + const text = getTextFromHtml(content); + if ( text ) { + addQuestion(text); + setIsLoading(true); + const response = await getCompletion(text); + addAnswer(response, text); + } + } + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }, []); + + React.useEffect(() => { + (async () => { + if (hasParentMessage && !executeAutoGetComplete.current) { + executeAutoGetComplete.current = true; + await runAutoGetComplete(); + } + })(); + }, [hasParentMessage]); + if (!isShowMessages) { return null; } @@ -104,7 +154,7 @@ export const RenderMessages: React.FunctionComponent = ( - + {hasError ? ( ) : ( diff --git a/samples/react-chatgpt-app/src/components/SendMessageToChat/SendMessageToChat.tsx b/samples/react-chatgpt-app/src/components/SendMessageToChat/SendMessageToChat.tsx index ed2812091..03b1222f6 100644 --- a/samples/react-chatgpt-app/src/components/SendMessageToChat/SendMessageToChat.tsx +++ b/samples/react-chatgpt-app/src/components/SendMessageToChat/SendMessageToChat.tsx @@ -18,7 +18,7 @@ export const SendMessageToChat: React.FunctionComponent = ( props: React.PropsWithChildren ) => { const [appGlobalState] = useAtom(globalState); - const { hasTeamsContext, chatId, } = appGlobalState; + const { hasTeamsContext, chatId,teamsId,channelId, } = appGlobalState; const { onSendMessage } = props; const shareIcon: IIconProps = React.useMemo(() => {return { iconName: "Share" }}, []); @@ -26,8 +26,8 @@ export const SendMessageToChat: React.FunctionComponent = ( const tooltipId = useId("tooltip"); const isInChat = React.useMemo(() => { - return hasTeamsContext && chatId; - }, [chatId, hasTeamsContext]); + return hasTeamsContext && (chatId || teamsId || channelId); + }, [chatId,teamsId,channelId, hasTeamsContext]); if (!isInChat) { return null; diff --git a/samples/react-chatgpt-app/src/hooks/useAdaptiveCardsUtils.ts b/samples/react-chatgpt-app/src/hooks/useAdaptiveCardsUtils.ts index 514c181d0..0dff3c3c4 100644 --- a/samples/react-chatgpt-app/src/hooks/useAdaptiveCardsUtils.ts +++ b/samples/react-chatgpt-app/src/hooks/useAdaptiveCardsUtils.ts @@ -19,7 +19,7 @@ const onProcessMarkdownHandler = (md:any, result: { outputHtml: string; didProc result.didProcess = false; } }; -export const useAdaptiveCardsUtils = function () { +export const useAdaptiveCardsUtils = () => { const createAdaptiveCard = React.useCallback((adaptiveCardData, card) => { const adaptiveCardToRender = new adaptiveCards.AdaptiveCard(); diff --git a/samples/react-chatgpt-app/src/hooks/useGraphAPI.ts b/samples/react-chatgpt-app/src/hooks/useGraphAPI.ts index 65f1bcde2..5e5930c78 100644 --- a/samples/react-chatgpt-app/src/hooks/useGraphAPI.ts +++ b/samples/react-chatgpt-app/src/hooks/useGraphAPI.ts @@ -8,27 +8,52 @@ import { } from '@microsoft/microsoft-graph-types'; import { BaseComponentContext } from '@microsoft/sp-component-base'; -export const useGraphAPI = (context: BaseComponentContext) => { +export const useGraphAPI = (context: BaseComponentContext) => { + const graphClient = React.useMemo(() => { + return async () => { + const client = await context.msGraphClientFactory.getClient("3"); + return client; + }; + }, [context]); + const sendMessageToChat = React.useCallback( + async (chatId: string, chatMessagePayload: object): Promise => { + const chatMessage = await (await graphClient()).api(`/chats/${chatId}/messages`).post(chatMessagePayload); + return chatMessage; + }, + [graphClient] + ); - const sendMessage = React.useCallback(async (chatId: string, message: string):Promise => { - const client = await context.msGraphClientFactory.getClient("3"); - const response:ChatMessage = await client.api(`/chats/${chatId}/messages`) - .post({ - body: { - content: `${message} (source: ChatGPT)` , - }, - }); - return response; - },[context]); + const sendMessageToChannel = React.useCallback( + async (teamsId: string, channelId: string, chatMessagePayload: object): Promise => { + const channelMessage = await (await graphClient()) + .api(`/teams/${teamsId}/channels/${channelId}/messages`) + .post(chatMessagePayload); + return channelMessage; + }, + [graphClient] + ); + const replyToMessage = React.useCallback( async (teamsId: string, channelId: string, parentMessageId: string, chatMessagePayload: object) => { + return (await graphClient()) + .api(`/teams/${teamsId}/channels/${channelId}/messages/${parentMessageId}/replies`) + .post(chatMessagePayload); + }, []); -const getChatInfo = React.useCallback(async (chatId:string):Promise => { - const client = await context.msGraphClientFactory.getClient("3"); - const response:Chat = await client.api(`/chats/${chatId}?$expand=members`) - .get(); - return response; - },[context]); + const getChatInfo = React.useCallback( + async (chatId: string): Promise => { + const response: Chat = await (await graphClient()).api(`/chats/${chatId}`).get(); + return response; + }, + [context] + ); - return {sendMessage, getChatInfo} + const getChatParentMessage = React.useCallback(async (chatId: string, parentMessageId: string):Promise => { + return (await graphClient()).api(`/chats/${chatId}/messages/${parentMessageId}`).get(); + }, []); + + const getChannelParentMessage = React.useCallback(async (teamId: string,channelId:string, parentMessageId: string):Promise => { + return (await graphClient()).api(`/teams/${teamId}/channels/${channelId}/messages/${parentMessageId}`).get(); + }, []); + + return { sendMessageToChat, sendMessageToChannel, replyToMessage, getChatInfo, getChatParentMessage, getChannelParentMessage }; }; - diff --git a/samples/react-chatgpt-app/src/hooks/useHtmlUtils.ts b/samples/react-chatgpt-app/src/hooks/useHtmlUtils.ts new file mode 100644 index 000000000..c17467116 --- /dev/null +++ b/samples/react-chatgpt-app/src/hooks/useHtmlUtils.ts @@ -0,0 +1,13 @@ + +interface IHtmlUtils { + getTextFromHtml: (html: string) => string; +} +export const useHtmlUtils = ():IHtmlUtils => { + + const getTextFromHtml = (html: string):string => { + const tmp = document.createElement('DIV'); + tmp.innerHTML = html; + return tmp.textContent || tmp.innerText || undefined; + }; + return {getTextFromHtml} +}; diff --git a/samples/react-chatgpt-app/src/hooks/useSendMessageToTeams.ts b/samples/react-chatgpt-app/src/hooks/useSendMessageToTeams.ts index 3012753e9..eb11a0120 100644 --- a/samples/react-chatgpt-app/src/hooks/useSendMessageToTeams.ts +++ b/samples/react-chatgpt-app/src/hooks/useSendMessageToTeams.ts @@ -4,17 +4,15 @@ import { BaseComponentContext } from '@microsoft/sp-component-base'; import { Guid } from '@microsoft/sp-core-library'; import { isEmpty } from '@microsoft/sp-lodash-subset'; +import { useGraphAPI } from '../hooks/useGraphAPI'; import { IAdaptativeCardData } from '../models/IAdaptivecardData'; import { HostedContents } from '../models/IChatMessage'; /* eslint-disable @typescript-eslint/explicit-function-return-type */ export const useSendMessageToTeams = (context: BaseComponentContext) => { - const graphClient = React.useMemo(() => { - return async () => { - const client = await context.msGraphClientFactory.getClient("3"); - return client; - }; - }, [context]); + +const { sendMessageToChat, sendMessageToChannel, replyToMessage } = useGraphAPI(context); + const getHostedContent = React.useCallback(async (adaptiveCard: object, adaptiveCardData: IAdaptativeCardData) => { try { @@ -55,50 +53,17 @@ export const useSendMessageToTeams = (context: BaseComponentContext) => { [] ); -/* const createChatMembers = React.useCallback((receiverEmail: string) => { - try { - const currentUser = context.pageContext.user.email; - const chatMembers = [ - { - "@odata.type": "#microsoft.graph.aadUserConversationMember", - roles: ["owner"], - "user@odata.bind": `https://graph.microsoft.com/v1.0/users('${currentUser}')`, - }, - { - "@odata.type": "#microsoft.graph.aadUserConversationMember", - roles: ["owner"], - "user@odata.bind": `https://graph.microsoft.com/v1.0/users('${receiverEmail}')`, - }, - ]; - return chatMembers; - } catch (error) { - if (DEBUG) { - console.error(`[SendMessage.createChatMembers]: error=${error}`); - throw error; - } - } - }, []); */ -/* - const createChat = React.useCallback( - async (receiverEmail) => { - try { - const members = createChatMembers(receiverEmail); - const chat = await (await graphClient()).api("/chats").post({ chatType: "oneOnOne", members: members }); - return chat; - } catch (error) { - if (DEBUG) { - console.error("[SendMessage.createChat]: error=", error); - throw error; - } - } - }, - [graphClient] - ); - */ - const sendMessage = React.useCallback( - async (adaptiveCard: object, adaptiveCardData: IAdaptativeCardData, chatId: string) => { - try { + const sendMessage = React.useCallback( + async ( + adaptiveCard: object, + adaptiveCardData: IAdaptativeCardData, + chatId: string, + teamsId: string, + channelId: string, + parentMessageId: string + ) => { + try { const { body, attachments, hostedContents } = await getSendMessagePayload(adaptiveCard, adaptiveCardData); const chatMessagePayload = { subject: "OpenAI Answer", @@ -106,8 +71,20 @@ export const useSendMessageToTeams = (context: BaseComponentContext) => { attachments: attachments, hostedContents: hostedContents, }; - const chatMessage = await (await graphClient()).api(`/chats/${chatId}/messages`).post(chatMessagePayload); - return chatMessage; + if (chatId && !teamsId && !channelId) { + console.log('channelId', channelId); + console.log('teamsId', teamsId); + const chatMessage = await sendMessageToChat(chatId, chatMessagePayload, ); + return chatMessage; + } + if (teamsId && channelId && !parentMessageId) { + const channelMessage = sendMessageToChannel(teamsId, channelId, chatMessagePayload); + return channelMessage; + } + if (teamsId && channelId && parentMessageId) { + const replyMessage = await replyToMessage(teamsId, channelId, parentMessageId, chatMessagePayload); + return replyMessage; + } } catch (error) { if (DEBUG) { console.error("[SendMessage]: error=", error); @@ -115,13 +92,20 @@ export const useSendMessageToTeams = (context: BaseComponentContext) => { } } }, - [graphClient] + [getSendMessagePayload, sendMessageToChannel, sendMessageToChat, replyToMessage, ] ); const sendAdativeCardToUsers = React.useCallback( - async (adaptiveCard: object, adaptiveCardData: IAdaptativeCardData, chatId: string) => { + async ( + adaptiveCard: object, + adaptiveCardData: IAdaptativeCardData, + chatId: string, + teamsId: string, + channelId: string, + parentMessageId: string + ) => { try { - await sendMessage(adaptiveCard, adaptiveCardData, chatId); + await sendMessage(adaptiveCard, adaptiveCardData, chatId, teamsId, channelId, parentMessageId); } catch (error) { if (DEBUG) { console.error(`[SendMessage.sendAdativeCardToUsers]: error=${error.message}`); @@ -129,7 +113,7 @@ export const useSendMessageToTeams = (context: BaseComponentContext) => { } } }, - [graphClient] + [sendMessage] ); return { sendAdativeCardToUsers }; diff --git a/samples/react-chatgpt-app/src/models/IAdaptivecardData.ts b/samples/react-chatgpt-app/src/models/IAdaptivecardData.ts index ac56618f8..425f98697 100644 --- a/samples/react-chatgpt-app/src/models/IAdaptivecardData.ts +++ b/samples/react-chatgpt-app/src/models/IAdaptivecardData.ts @@ -1,4 +1,5 @@ export interface IAdaptativeCardData { date: string; answer: string; + question: string; } diff --git a/samples/react-chatgpt-app/src/models/IChatGptProps.ts b/samples/react-chatgpt-app/src/models/IChatGptProps.ts index f76b4591f..0c2c4c6eb 100644 --- a/samples/react-chatgpt-app/src/models/IChatGptProps.ts +++ b/samples/react-chatgpt-app/src/models/IChatGptProps.ts @@ -12,4 +12,7 @@ export interface IChatGptProps { context: BaseComponentContext; theme: ITheme | IReadonlyTheme ; chatId: string; + teamsId: string; + channelId: string; + parentMessageId: string; } diff --git a/samples/react-chatgpt-app/src/models/IGlobalState.ts b/samples/react-chatgpt-app/src/models/IGlobalState.ts index 8e168614b..a292d4a17 100644 --- a/samples/react-chatgpt-app/src/models/IGlobalState.ts +++ b/samples/react-chatgpt-app/src/models/IGlobalState.ts @@ -11,6 +11,9 @@ export interface IGlobalState { isDarkTheme: boolean; hasTeamsContext: boolean; chatId: string; + teamsId: string; + channelId: string; + parentMessageId: string; appId: string; AzureFunctionUrl: string; } diff --git a/samples/react-chatgpt-app/src/models/IRenderAnswerProps.ts b/samples/react-chatgpt-app/src/models/IRenderAnswerProps.ts index ab5f55aa9..fa34143bd 100644 --- a/samples/react-chatgpt-app/src/models/IRenderAnswerProps.ts +++ b/samples/react-chatgpt-app/src/models/IRenderAnswerProps.ts @@ -1,3 +1,4 @@ export interface IRenderAnswerProps { answer: string; + question?:string } diff --git a/samples/react-chatgpt-app/src/webparts/chatGpt/ChatGptWebPart.ts b/samples/react-chatgpt-app/src/webparts/chatGpt/ChatGptWebPart.ts index 3a05326ab..87781e989 100644 --- a/samples/react-chatgpt-app/src/webparts/chatGpt/ChatGptWebPart.ts +++ b/samples/react-chatgpt-app/src/webparts/chatGpt/ChatGptWebPart.ts @@ -66,6 +66,9 @@ export default class ChatGptWebPart extends BaseClientSideWebPart theme: this._currentTheme, context: this.context, chatId: this._chatId, + teamsId: this._teamId, + channelId: this._channelId, + parentMessageId: this._parentMessageId, }); ReactDom.render(element, this.domElement); @@ -84,6 +87,7 @@ export default class ChatGptWebPart extends BaseClientSideWebPart this._channelId = teamsContext.channel?.id; this._parentMessageId = teamsContext.app.parentMessageId; + console.log("chatId", this._chatId); console.log("teamId", this._teamId); console.log("channelId", this._channelId);