Skip to content

Commit

Permalink
Adding new PCF control
Browse files Browse the repository at this point in the history
  • Loading branch information
Power-Maverick committed Mar 7, 2023
1 parent 064e38f commit 7b5e45c
Show file tree
Hide file tree
Showing 17 changed files with 10,183 additions and 0 deletions.
19 changes: 19 additions & 0 deletions ChatGPTControl/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended"],
"globals": {
"ComponentFramework": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": ["@microsoft/power-apps", "@typescript-eslint"],
"rules": {
"no-unused-vars": "off"
}
}
17 changes: 17 additions & 0 deletions ChatGPTControl/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules

# generated directory
**/generated

# output directory
/out

# msbuild output directories
/bin
/obj

# MSBuild Binary and Structured Log
*.binlog
8 changes: 8 additions & 0 deletions ChatGPTControl/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"semi": true,
"trailingComma": "all",
"singleQuote": false,
"printWidth": 250,
"tabWidth": 4,
"endOfLine": "auto"
}
Binary file added ChatGPTControl/Assets/PCF-GPT-Demo1.mp4
Binary file not shown.
49 changes: 49 additions & 0 deletions ChatGPTControl/ChatGPT/ControlManifest.Input.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest>
<control namespace="Maverick.Controls" constructor="ChatGPT" version="0.0.2" display-name-key="ChatGPT" description-key="ChatGPT description" control-type="standard">
<!--external-service-usage node declares whether this 3rd party PCF control is using external service or not, if yes, this control will be considered as premium and please also add the external domain it is using.
If it is not using any external service, please set the enabled="false" and DO NOT add any domain below. The "enabled" will be false by default.
Example1:
<external-service-usage enabled="true">
<domain>www.Microsoft.com</domain>
</external-service-usage>
Example2:
<external-service-usage enabled="false">
</external-service-usage>
-->
<external-service-usage enabled="false">
<!--UNCOMMENT TO ADD EXTERNAL DOMAINS
<domain></domain>
<domain></domain>
-->
</external-service-usage>
<!-- property node identifies a specific, configurable piece of data that the control expects from CDS -->
<property name="primary" display-name-key="Primary" description-key="Primary" usage="bound" required="true" of-type-group="Text" />
<property name="organization" display-name-key="Chat GPT Organization" description-key="Chat GPT Organization" usage="input" required="true" of-type="SingleLine.Text" />
<property name="apikey" display-name-key="Chat GPT API Key" description-key="Chat GPT API Key" usage="input" required="true" of-type="SingleLine.Text" />
<resources>
<code path="index.ts" order="1" />
<!-- UNCOMMENT TO ADD MORE RESOURCES
<css path="css/ChatGPT.css" order="1" />
<resx path="strings/ChatGPT.1033.resx" version="1.0.0" />
-->
</resources>
<!-- UNCOMMENT TO ENABLE THE SPECIFIED API
<feature-usage>
<uses-feature name="Device.captureAudio" required="true" />
<uses-feature name="Device.captureImage" required="true" />
<uses-feature name="Device.captureVideo" required="true" />
<uses-feature name="Device.getBarcodeValue" required="true" />
<uses-feature name="Device.getCurrentPosition" required="true" />
<uses-feature name="Device.pickFile" required="true" />
<uses-feature name="Utility" required="true" />
<uses-feature name="WebAPI" required="true" />
</feature-usage>
-->
<type-group name="Text">
<type>SingleLine.Text</type>
<type>SingleLine.TextArea</type>
<type>Multiple</type>
</type-group>
</control>
</manifest>
205 changes: 205 additions & 0 deletions ChatGPTControl/ChatGPT/components/chat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import React, { useEffect } from "react";
import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai";
import { FocusZone, FocusZoneDirection } from "@fluentui/react/lib/FocusZone";
import { List } from "@fluentui/react/lib/List";
import { getFocusStyle, getTheme, ITheme, mergeStyleSets } from "@fluentui/react/lib/Styling";
import { TextField } from "@fluentui/react/lib/TextField";
import { IconButton } from "@fluentui/react/lib/Button";
import { IIconProps } from "@fluentui/react/lib/Icon";
import { Image, IImageProps } from "@fluentui/react/lib/Image";
import { Spinner } from "@fluentui/react/lib/Spinner";

export interface IChatListProps {
chatMessages?: Array<ChatCompletionRequestMessage>;
organization?: string;
apiKey?: string;
relayUpdates: (newData: ChatCompletionRequestMessage) => void;
}

const theme: ITheme = getTheme();
const { palette, semanticColors, fonts } = theme;
const classNames = mergeStyleSets({
itemCell: [
getFocusStyle(theme, { inset: -1 }),
{
minHeight: 54,
padding: 10,
boxSizing: "border-box",
borderBottom: `1px solid ${semanticColors.bodyDivider}`,
display: "flex",
selectors: {
"&:hover": { background: palette.neutralLight },
},
},
],
itemImage: {
flexShrink: 0,
paddingRight: 10,
},
itemContent: {
marginLeft: 10,
overflow: "hidden",
flexGrow: 1,
},
itemTitle: [
fonts.xLarge,
{
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
textAlign: "left",
display: "flex",
},
],
itemMessage: {
textAlign: "left",
},
msgWrapper: {
display: "flex",
},
msgText: {
width: "50%",
},
search: {
textAlign: "left",
width: "50%",
},
msgEmpty: {
textAlign: "left",
width: "50%",
paddingBottom: "10px",
},
spinnerWrapper: {
display: "flex",
paddingTop: "10px",
paddingBottom: "10px",
},
});

const ChatMessages = (props: IChatListProps): JSX.Element => {
const configuration = new Configuration({
organization: props.organization!,
apiKey: props.apiKey!,
});
const openai = new OpenAIApi(configuration);

const submitIcon: IIconProps = { iconName: "Send" };
const [items, setItems] = React.useState(props.chatMessages ?? []);
const [sendNewMessage, setSendNewMessage] = React.useState(false);
const [textFieldValue, setTextFieldValue] = React.useState("");

const userImageProps: Partial<IImageProps> = {
src: "https://img.freepik.com/premium-vector/avatar-profile-icon_188544-4755.jpg",
width: 32,
};

const gptImageProps: Partial<IImageProps> = {
src: "https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/ChatGPT_logo.svg/1024px-ChatGPT_logo.svg.png",
width: 32,
};

// useEffect(() => {
// if (props.chatMessages) {
// openai
// .createChatCompletion({
// model: "gpt-3.5-turbo",
// messages: props.chatMessages,
// })
// .then((response) => {
// response.data.choices.forEach((choice) => {
// setItems((items) => [...items, choice.message as ChatCompletionRequestMessage]);
// });
// });
// }
// }, [props.chatMessages]);

useEffect(() => {
if (items.length > 0 && sendNewMessage) {
openai
.createChatCompletion({
model: "gpt-3.5-turbo",
messages: items,
})
.then((response) => {
response.data.choices.forEach((choice) => {
setItems((items) => [...items, choice.message as ChatCompletionRequestMessage]);
});
props.relayUpdates(response.data.choices[0].message as ChatCompletionRequestMessage);
setSendNewMessage(false);
});
}
}, [sendNewMessage]);

const onFilterChanged = (event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, text?: string): void => {
if (text) {
setItems(items.filter((item) => item.content.toLowerCase().indexOf(text.toLowerCase()) >= 0));
} else {
setItems(props.chatMessages!);
}
};

const onRenderCell = (item?: ChatCompletionRequestMessage, index?: number | undefined): JSX.Element => {
if (!item) {
return <></>;
} else {
return (
<div className={classNames.itemCell} data-is-focusable={true}>
<div className={classNames.itemContent}>
<div className={classNames.itemTitle}>
{item?.role === "user" ? <Image {...userImageProps} alt={item?.role} className={classNames.itemImage} /> : <Image {...gptImageProps} alt={item?.role} className={classNames.itemImage} />}
{item?.role}
</div>
{/* <div className={classNames.itemIndex}>{`Item ${index}`}</div> */}
<div className={classNames.itemMessage}>{item?.content}</div>
</div>
</div>
);
}
};

const onChangeTextFieldValue = React.useCallback((event: React.FormEvent<HTMLInputElement | HTMLTextAreaElement>, newValue?: string) => {
setTextFieldValue(newValue || "");
}, []);

const sendToChatGPT = (): void => {
const message: ChatCompletionRequestMessage = { role: "user", content: textFieldValue };
setItems((items) => [...items, message]);
setSendNewMessage(true);
setTextFieldValue("");
props.relayUpdates(message);
};

return (
<FocusZone direction={FocusZoneDirection.vertical}>
{items && items.length > 0 ? (
<>
<TextField
className={classNames.search}
label={"Search"}
placeholder={"Search your content..."}
// eslint-disable-next-line react/jsx-no-bind
onChange={onFilterChanged}
/>
<List items={items} onRenderCell={onRenderCell} />
</>
) : (
<div className={classNames.msgEmpty}>Ask your questions to Chat GPT</div>
)}
{sendNewMessage ? (
<div className={classNames.spinnerWrapper}>
<Spinner label="Chat GPT is responding... Please wait..." ariaLive="assertive" labelPosition="left" />
</div>
) : (
<></>
)}
<div className={classNames.msgWrapper}>
<TextField className={classNames.msgText} placeholder={"Type your message here"} value={textFieldValue} onChange={onChangeTextFieldValue} />
<IconButton iconProps={submitIcon} title="Send" ariaLabel="Send" onClick={sendToChatGPT} />
</div>
</FocusZone>
);
};

export default ChatMessages;
90 changes: 90 additions & 0 deletions ChatGPTControl/ChatGPT/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { ChatCompletionRequestMessage } from "openai";
import React from "react";
import ReactDOM from "react-dom";
import ChatMessages, { IChatListProps } from "./components/chat";
import { IInputs, IOutputs } from "./generated/ManifestTypes";

export class ChatGPT implements ComponentFramework.StandardControl<IInputs, IOutputs> {
/**
* Global Variables
*/
private theContainer: HTMLDivElement;
private theNotifyOutputChanged: () => void;
private changedItems: ChatCompletionRequestMessage[] = [];

/**
* React properties
*/
private props: IChatListProps = {
relayUpdates: this.getChangedDataFromReactComponent.bind(this),
};

/**
* Empty constructor.
*/
constructor() {}

/**
* Used to initialize the control instance. Controls can kick off remote server calls and other initialization actions here.
* Data-set values are not initialized here, use updateView.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to property names defined in the manifest, as well as utility functions.
* @param notifyOutputChanged A callback method to alert the framework that the control has new outputs ready to be retrieved asynchronously.
* @param state A piece of data that persists in one session for a single user. Can be set at any point in a controls life cycle by calling 'setControlState' in the Mode interface.
* @param container If a control is marked control-type='standard', it will receive an empty div element within which it can render its content.
*/
public init(context: ComponentFramework.Context<IInputs>, notifyOutputChanged: () => void, state: ComponentFramework.Dictionary, container: HTMLDivElement): void {
this.theNotifyOutputChanged = notifyOutputChanged;
this.theContainer = container;
}

/**
* Called when any value in the property bag has changed. This includes field values, data-sets, global values such as container height and width, offline status, control metadata values such as label, visible, etc.
* @param context The entire property bag available to control via Context Object; It contains values as set up by the customizer mapped to names defined in the manifest, as well as utility functions
*/
public updateView(context: ComponentFramework.Context<IInputs>): void {
if (context.parameters.primary.raw) {
this.props.chatMessages = JSON.parse(context.parameters.primary.raw);
this.changedItems = JSON.parse(context.parameters.primary.raw);
}
this.props.apiKey = context.parameters.apikey.raw ?? "";
this.props.organization = context.parameters.organization.raw ?? "";

this.renderReactDOM();
}

/**
* It is called by the framework prior to a control receiving new data.
* @returns an object based on nomenclature defined in manifest, expecting object[s] for property marked as “bound” or “output”
*/
public getOutputs(): IOutputs {
return {
primary: JSON.stringify(this.changedItems),
};
}

/**
* Called when the control is to be removed from the DOM tree. Controls should use this call for cleanup.
* i.e. cancelling any pending remote calls, removing listeners, etc.
*/
public destroy(): void {
// Add code to cleanup control if necessary
}

//#region Private methods

private getChangedDataFromReactComponent(newData: ChatCompletionRequestMessage): void {
this.changedItems = [...this.changedItems, newData];
this.theNotifyOutputChanged();
}

private renderReactDOM(): void {
ReactDOM.render(
// Create the React component
React.createElement(ChatMessages, this.props),
this.theContainer,
);
}

//#endregion
}
Loading

0 comments on commit 7b5e45c

Please sign in to comment.