commit changes on CHatGPT-APP - add support to Chat and Channels

This commit is contained in:
João Mendes 2023-03-05 16:42:45 +00:00
parent dd66bc3881
commit 8a64316f4d
14 changed files with 199 additions and 96 deletions

View File

@ -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",

View File

@ -26,14 +26,19 @@ export const ChatGptControl: React.FunctionComponent<IChatGptProps> = (
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<Error | undefined>(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<IChatGptProps> = (
return (
<>
<Stack tokens={{ childrenGap: 20 }} styles={containerStyles}>
<Header isInChat={isInChat} />
<Header isInChat={isInChat || isInChannel} />
<RenderPreviewChatInfo isPreviewChatId={isPreviewChatId} />
<RenderMessages isShowMessages={!isPreviewChatId} />
</Stack>

View File

@ -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<IRenderAnswerProps> = (
props: React.PropsWithChildren<IRenderAnswerProps>
) => {
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<Error | undefined>(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<IRenderAnswerProps> = (
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<IRenderAnswerProps> = (
<SendMessageToChat onSendMessage={onSendMessageToChat} />
</Stack>
</Stack>
<Stack horizontalAlign="start" tokens={{ childrenGap: 10 }}>
<div
dangerouslySetInnerHTML={{ __html: answer?.replace("\n\n", " ") }}

View File

@ -1,3 +1,5 @@
/* eslint-disable require-atomic-updates */
/* eslint-disable @typescript-eslint/no-floating-promises */
import * as React from 'react';
import { useAtom } from 'jotai';
@ -6,8 +8,12 @@ import { IIconProps } from 'office-ui-fabric-react/lib/Icon';
import { Stack } from 'office-ui-fabric-react/lib/Stack';
import { TextField } from 'office-ui-fabric-react/lib/TextField';
import { ChatMessage } from '@microsoft/microsoft-graph-types';
import { globalState } from '../../atoms/globalState';
import { useChatGpt } from '../../hooks';
import { useGraphAPI } from '../../hooks/useGraphAPI';
import { useHtmlUtils } from '../../hooks/useHtmlUtils';
import { useChatGptStyles } from '../ChatGpt/useChatGptStyles';
import { ErrorMessage } from '../ErrorMessage/ErrorMessage';
import { Loading } from '../LoadingAnswer/Loading';
@ -23,7 +29,7 @@ export const RenderMessages: React.FunctionComponent<IRenderMessagesProps> = (
) => {
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<React.ReactNode[]>([]);
const [textToAsk, setTextToAsk] = React.useState<string>("");
@ -31,6 +37,12 @@ export const RenderMessages: React.FunctionComponent<IRenderMessagesProps> = (
const { getCompletion } = useChatGpt(context, appId, AzureFunctionUrl);
const scrollRef = React.useRef<HTMLDivElement>(null);
const [error, setError] = React.useState<Error | undefined>(undefined);
const { getChatParentMessage, getChannelParentMessage } = useGraphAPI(context);
const executeAutoGetComplete = React.useRef<boolean>(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<IRenderMessagesProps> = (
);
const addAnswer = React.useCallback(
(answer: string) => {
const newAnswer = <RenderAnswer answer={answer} key={conversation.length + 1} />;
(answer: string, question?: string) => {
const newAnswer = <RenderAnswer answer={answer} question={question} key={conversation.length + 1} />;
setConversation((prev) => {
return [...prev, newAnswer];
});
@ -92,6 +104,44 @@ export const RenderMessages: React.FunctionComponent<IRenderMessagesProps> = (
[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<IRenderMessagesProps> = (
<Stack tokens={{ padding: 20, childrenGap: 10 }}>
<Loading isLoading={isLoading} />
<Stack horizontal tokens={{ childrenGap: 5 }} >
<Stack horizontal tokens={{ childrenGap: 5 }}>
{hasError ? (
<ErrorMessage errorMessage={error?.message} showError={hasError} />
) : (

View File

@ -18,7 +18,7 @@ export const SendMessageToChat: React.FunctionComponent<ISendMessageProps> = (
props: React.PropsWithChildren<ISendMessageProps>
) => {
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<ISendMessageProps> = (
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;

View File

@ -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();

View File

@ -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<ChatMessage> => {
const chatMessage = await (await graphClient()).api(`/chats/${chatId}/messages`).post(chatMessagePayload);
return chatMessage;
},
[graphClient]
);
const sendMessage = React.useCallback(async (chatId: string, message: string):Promise<ChatMessage> => {
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<ChatMessage> => {
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<Chat> => {
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<Chat> => {
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<ChatMessage> => {
return (await graphClient()).api(`/chats/${chatId}/messages/${parentMessageId}`).get();
}, []);
const getChannelParentMessage = React.useCallback(async (teamId: string,channelId:string, parentMessageId: string):Promise<ChatMessage> => {
return (await graphClient()).api(`/teams/${teamId}/channels/${channelId}/messages/${parentMessageId}`).get();
}, []);
return { sendMessageToChat, sendMessageToChannel, replyToMessage, getChatInfo, getChatParentMessage, getChannelParentMessage };
};

View File

@ -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}
};

View File

@ -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 };

View File

@ -1,4 +1,5 @@
export interface IAdaptativeCardData {
date: string;
answer: string;
question: string;
}

View File

@ -12,4 +12,7 @@ export interface IChatGptProps {
context: BaseComponentContext;
theme: ITheme | IReadonlyTheme ;
chatId: string;
teamsId: string;
channelId: string;
parentMessageId: string;
}

View File

@ -11,6 +11,9 @@ export interface IGlobalState {
isDarkTheme: boolean;
hasTeamsContext: boolean;
chatId: string;
teamsId: string;
channelId: string;
parentMessageId: string;
appId: string;
AzureFunctionUrl: string;
}

View File

@ -1,3 +1,4 @@
export interface IRenderAnswerProps {
answer: string;
question?:string
}

View File

@ -66,6 +66,9 @@ export default class ChatGptWebPart extends BaseClientSideWebPart<IChatGptProps>
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<IChatGptProps>
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);